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,346 @@
|
|
|
1
|
+
"""GitHub issue client service for roadmap application.
|
|
2
|
+
|
|
3
|
+
This module handles fetching and syncing GitHub issues with internal roadmap issues.
|
|
4
|
+
It provides a simple interface for GitHub issue operations without requiring a
|
|
5
|
+
GitHub repository to be configured.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from roadmap.adapters.github.handlers.base import GitHubAPIError
|
|
12
|
+
from roadmap.common.logging import get_logger
|
|
13
|
+
from roadmap.common.logging.error_logging import (
|
|
14
|
+
log_external_service_error,
|
|
15
|
+
log_validation_error,
|
|
16
|
+
)
|
|
17
|
+
from roadmap.common.observability.instrumentation import traced
|
|
18
|
+
from roadmap.infrastructure.github_gateway import GitHubGateway
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from roadmap.core.interfaces import GitHubBackendInterface # noqa: F401
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GitHubIssueClient:
|
|
27
|
+
"""Client for fetching and managing GitHub issues.
|
|
28
|
+
|
|
29
|
+
This client allows operations on GitHub issues without requiring
|
|
30
|
+
owner/repo configuration upfront. Configuration can be provided
|
|
31
|
+
per-request or via environment variables.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
token: str | None = None,
|
|
37
|
+
github_backend: "GitHubBackendInterface | None" = None,
|
|
38
|
+
):
|
|
39
|
+
"""Initialize GitHub issue client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
token: Optional GitHub personal access token.
|
|
43
|
+
If not provided, will look for GITHUB_TOKEN environment variable.
|
|
44
|
+
github_backend: Optional GitHub backend interface. If not provided,
|
|
45
|
+
will create GitHubBackendAdapter instances per-request.
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
GitHubAPIError: If no token is available
|
|
49
|
+
"""
|
|
50
|
+
self.token = token or os.getenv("GITHUB_TOKEN")
|
|
51
|
+
if not self.token:
|
|
52
|
+
raise GitHubAPIError(
|
|
53
|
+
"GitHub token is required. Set GITHUB_TOKEN environment variable "
|
|
54
|
+
"or provide token to GitHubIssueClient."
|
|
55
|
+
)
|
|
56
|
+
self._github_backend = github_backend
|
|
57
|
+
logger.debug("github_issue_client_initialized")
|
|
58
|
+
|
|
59
|
+
@traced("fetch_issue")
|
|
60
|
+
def fetch_issue(self, owner: str, repo: str, issue_number: int) -> dict[str, Any]:
|
|
61
|
+
"""Fetch a GitHub issue by number.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
owner: Repository owner (username or organization)
|
|
65
|
+
repo: Repository name
|
|
66
|
+
issue_number: GitHub issue number (must be positive integer)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary with issue details:
|
|
70
|
+
- number: Issue number
|
|
71
|
+
- title: Issue title
|
|
72
|
+
- body: Issue description
|
|
73
|
+
- state: 'open' or 'closed'
|
|
74
|
+
- labels: List of label names
|
|
75
|
+
- assignees: List of assignee usernames
|
|
76
|
+
- milestone: Milestone title or None
|
|
77
|
+
- url: GitHub issue URL
|
|
78
|
+
- created_at: ISO format timestamp
|
|
79
|
+
- updated_at: ISO format timestamp
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
GitHubAPIError: If issue not found or API error occurs
|
|
83
|
+
ValueError: If issue_number is invalid
|
|
84
|
+
"""
|
|
85
|
+
logger.info(
|
|
86
|
+
"fetching_github_issue", owner=owner, repo=repo, number=issue_number
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Use injected backend or create adapter on-demand
|
|
91
|
+
if self._github_backend is None:
|
|
92
|
+
client = GitHubGateway.get_github_client(
|
|
93
|
+
{"token": self.token, "owner": owner, "repo": repo}
|
|
94
|
+
)
|
|
95
|
+
issue_data = client.fetch_issue(issue_number)
|
|
96
|
+
else:
|
|
97
|
+
issue_data = self._github_backend.get_issue(str(issue_number))
|
|
98
|
+
|
|
99
|
+
logger.debug("github_issue_fetched_successfully", number=issue_number)
|
|
100
|
+
return issue_data if isinstance(issue_data, dict) else {}
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
log_validation_error(
|
|
103
|
+
e,
|
|
104
|
+
entity_type="GitHubIssue",
|
|
105
|
+
field_name="issue_number",
|
|
106
|
+
proposed_value=issue_number,
|
|
107
|
+
)
|
|
108
|
+
logger.warning(
|
|
109
|
+
"invalid_github_issue_number", error=str(e), severity="data_error"
|
|
110
|
+
)
|
|
111
|
+
raise
|
|
112
|
+
except GitHubAPIError as e:
|
|
113
|
+
log_external_service_error(
|
|
114
|
+
e,
|
|
115
|
+
service_name="GitHub",
|
|
116
|
+
operation="fetch_issue",
|
|
117
|
+
)
|
|
118
|
+
logger.warning(
|
|
119
|
+
"github_issue_fetch_failed", error=str(e), severity="infrastructure"
|
|
120
|
+
)
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
@traced("validate_token")
|
|
124
|
+
def validate_token(self) -> tuple[bool, str]:
|
|
125
|
+
"""Validate GitHub token by making a test API call.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Tuple of (is_valid, message)
|
|
129
|
+
- (True, "Token is valid (authenticated as USERNAME)") if successful
|
|
130
|
+
- (False, error_message) if invalid or error
|
|
131
|
+
"""
|
|
132
|
+
logger.info("validating_github_token")
|
|
133
|
+
try:
|
|
134
|
+
# Create a backend adapter with minimal configuration (needs owner/repo for initialization)
|
|
135
|
+
# We'll use a dummy repo for validation since we only care about authentication
|
|
136
|
+
client = GitHubGateway.get_github_client(
|
|
137
|
+
{"token": self.token, "owner": "dummy", "repo": "dummy"}
|
|
138
|
+
)
|
|
139
|
+
is_valid, message = client.validate_github_token()
|
|
140
|
+
|
|
141
|
+
if is_valid:
|
|
142
|
+
logger.debug("github_token_validated", authenticated_user=message)
|
|
143
|
+
else:
|
|
144
|
+
logger.warning(
|
|
145
|
+
"github_token_validation_failed",
|
|
146
|
+
error_message=message,
|
|
147
|
+
severity="config_error",
|
|
148
|
+
)
|
|
149
|
+
return is_valid, message
|
|
150
|
+
except GitHubAPIError as e:
|
|
151
|
+
log_external_service_error(
|
|
152
|
+
e,
|
|
153
|
+
service_name="GitHub",
|
|
154
|
+
operation="validate_token",
|
|
155
|
+
)
|
|
156
|
+
return False, str(e)
|
|
157
|
+
|
|
158
|
+
@traced("issue_exists")
|
|
159
|
+
def issue_exists(self, owner: str, repo: str, issue_number: int) -> bool:
|
|
160
|
+
"""Check if a GitHub issue exists.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
owner: Repository owner (username or organization)
|
|
164
|
+
repo: Repository name
|
|
165
|
+
issue_number: GitHub issue number
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
True if issue exists, False otherwise
|
|
169
|
+
"""
|
|
170
|
+
logger.info(
|
|
171
|
+
"checking_github_issue_exists", owner=owner, repo=repo, number=issue_number
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
self.fetch_issue(owner, repo, issue_number)
|
|
175
|
+
logger.debug("github_issue_exists", number=issue_number)
|
|
176
|
+
return True
|
|
177
|
+
except GitHubAPIError:
|
|
178
|
+
logger.debug("github_issue_does_not_exist", number=issue_number)
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
@traced("get_issue_diff")
|
|
182
|
+
def get_issue_diff(
|
|
183
|
+
self,
|
|
184
|
+
owner: str,
|
|
185
|
+
repo: str,
|
|
186
|
+
issue_number: int,
|
|
187
|
+
local_data: dict[str, Any],
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
"""Get differences between local issue data and GitHub issue.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
owner: Repository owner
|
|
193
|
+
repo: Repository name
|
|
194
|
+
issue_number: GitHub issue number
|
|
195
|
+
local_data: Local issue data to compare (keys: title, body, state, labels, assignees)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Dictionary with changes:
|
|
199
|
+
- has_changes: bool
|
|
200
|
+
- title_changed: bool (if different)
|
|
201
|
+
- body_changed: bool (if different)
|
|
202
|
+
- state_changed: bool (if different)
|
|
203
|
+
- labels_changed: bool (if different)
|
|
204
|
+
- assignees_changed: bool (if different)
|
|
205
|
+
- github_data: dict with full GitHub issue data
|
|
206
|
+
- changes: dict with field-level details
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
GitHubAPIError: If issue fetch fails
|
|
210
|
+
"""
|
|
211
|
+
logger.info(
|
|
212
|
+
"comparing_issue_with_github", owner=owner, repo=repo, number=issue_number
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
github_data = self.fetch_issue(owner, repo, issue_number)
|
|
216
|
+
|
|
217
|
+
changes = {}
|
|
218
|
+
has_changes = False
|
|
219
|
+
|
|
220
|
+
# Compare fields
|
|
221
|
+
fields_to_compare = ["title", "body", "state"]
|
|
222
|
+
for field in fields_to_compare:
|
|
223
|
+
local_value = local_data.get(field, "")
|
|
224
|
+
github_value = github_data.get(field, "")
|
|
225
|
+
if local_value != github_value:
|
|
226
|
+
changes[field] = {
|
|
227
|
+
"local": local_value,
|
|
228
|
+
"github": github_value,
|
|
229
|
+
"changed": True,
|
|
230
|
+
}
|
|
231
|
+
has_changes = True
|
|
232
|
+
|
|
233
|
+
# Compare lists (labels, assignees)
|
|
234
|
+
for field in ["labels", "assignees"]:
|
|
235
|
+
local_value = sorted(set(local_data.get(field, [])))
|
|
236
|
+
github_value = sorted(set(github_data.get(field, [])))
|
|
237
|
+
if local_value != github_value:
|
|
238
|
+
changes[field] = {
|
|
239
|
+
"local": local_value,
|
|
240
|
+
"github": github_value,
|
|
241
|
+
"changed": True,
|
|
242
|
+
}
|
|
243
|
+
has_changes = True
|
|
244
|
+
|
|
245
|
+
result = {
|
|
246
|
+
"has_changes": has_changes,
|
|
247
|
+
"github_data": github_data,
|
|
248
|
+
"changes": changes,
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.debug(
|
|
252
|
+
"issue_diff_computed", has_changes=has_changes, fields_changed=len(changes)
|
|
253
|
+
)
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
@traced("get_issues")
|
|
257
|
+
def get_issues(
|
|
258
|
+
self,
|
|
259
|
+
owner: str,
|
|
260
|
+
repo: str,
|
|
261
|
+
state: str = "all",
|
|
262
|
+
labels: list[str] | None = None,
|
|
263
|
+
milestone: str | None = None,
|
|
264
|
+
assignee: str | None = None,
|
|
265
|
+
per_page: int = 100,
|
|
266
|
+
) -> list[dict[str, Any]]:
|
|
267
|
+
"""Get issues from a GitHub repository.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
owner: Repository owner (username or organization)
|
|
271
|
+
repo: Repository name
|
|
272
|
+
state: Issue state filter ('open', 'closed', 'all')
|
|
273
|
+
labels: Optional list of label names to filter by
|
|
274
|
+
milestone: Optional milestone title to filter by
|
|
275
|
+
assignee: Optional assignee username to filter by
|
|
276
|
+
per_page: Number of issues per page (max 100)
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of issue dictionaries from GitHub API
|
|
280
|
+
|
|
281
|
+
Raises:
|
|
282
|
+
GitHubAPIError: If API call fails
|
|
283
|
+
"""
|
|
284
|
+
logger.info(
|
|
285
|
+
"fetching_github_issues",
|
|
286
|
+
owner=owner,
|
|
287
|
+
repo=repo,
|
|
288
|
+
state=state,
|
|
289
|
+
limit=per_page,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
# Use injected backend or create adapter on-demand
|
|
294
|
+
if self._github_backend is None:
|
|
295
|
+
client = GitHubGateway.get_github_client(
|
|
296
|
+
{"token": self.token, "owner": owner, "repo": repo}
|
|
297
|
+
)
|
|
298
|
+
issues_data = client.get_issues(
|
|
299
|
+
state, labels, milestone, assignee, per_page
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
# If backend provided, use it - backend get_issues() takes no parameters
|
|
303
|
+
if hasattr(self._github_backend, "get_issues"):
|
|
304
|
+
all_issues = self._github_backend.get_issues()
|
|
305
|
+
# Filter results if needed (backend returns all issues)
|
|
306
|
+
issues_data = (
|
|
307
|
+
all_issues
|
|
308
|
+
if isinstance(all_issues, list)
|
|
309
|
+
else list(all_issues.values())
|
|
310
|
+
if isinstance(all_issues, dict)
|
|
311
|
+
else []
|
|
312
|
+
)
|
|
313
|
+
else:
|
|
314
|
+
issues_data = []
|
|
315
|
+
|
|
316
|
+
logger.debug(
|
|
317
|
+
"github_issues_fetched",
|
|
318
|
+
owner=owner,
|
|
319
|
+
repo=repo,
|
|
320
|
+
count=len(issues_data or []),
|
|
321
|
+
)
|
|
322
|
+
return issues_data if isinstance(issues_data, list) else []
|
|
323
|
+
|
|
324
|
+
except GitHubAPIError as e:
|
|
325
|
+
log_external_service_error(
|
|
326
|
+
e,
|
|
327
|
+
service_name="GitHub",
|
|
328
|
+
operation="get_issues",
|
|
329
|
+
)
|
|
330
|
+
logger.warning(
|
|
331
|
+
"github_issues_fetch_failed", error=str(e), severity="infrastructure"
|
|
332
|
+
)
|
|
333
|
+
raise
|
|
334
|
+
except Exception as e:
|
|
335
|
+
log_external_service_error(
|
|
336
|
+
e,
|
|
337
|
+
service_name="GitHub",
|
|
338
|
+
operation="get_issues",
|
|
339
|
+
)
|
|
340
|
+
logger.error(
|
|
341
|
+
"github_issues_fetch_error",
|
|
342
|
+
error=str(e),
|
|
343
|
+
error_type=type(e).__name__,
|
|
344
|
+
severity="infrastructure",
|
|
345
|
+
)
|
|
346
|
+
raise GitHubAPIError(f"Failed to fetch issues: {str(e)}") from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Package initialization file."""
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Service for cleaning up old backup files."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from roadmap.common.logging import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BackupInfo(TypedDict):
|
|
13
|
+
"""Information about a backup file."""
|
|
14
|
+
|
|
15
|
+
path: Path
|
|
16
|
+
mtime: datetime
|
|
17
|
+
size: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BackupCleanupResult:
|
|
21
|
+
"""Result of backup cleanup operation."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize BackupCleanupResult."""
|
|
25
|
+
self.deleted_count: int = 0
|
|
26
|
+
self.failed_count: int = 0
|
|
27
|
+
self.total_freed_bytes: int = 0
|
|
28
|
+
self.files_deleted: list[Path] = []
|
|
29
|
+
self.files_failed: list[tuple[Path, str]] = []
|
|
30
|
+
|
|
31
|
+
def add_deleted(self, path: Path, size: int) -> None:
|
|
32
|
+
"""Record a deleted file."""
|
|
33
|
+
self.deleted_count += 1
|
|
34
|
+
self.total_freed_bytes += size
|
|
35
|
+
self.files_deleted.append(path)
|
|
36
|
+
|
|
37
|
+
def add_failed(self, path: Path, error: str) -> None:
|
|
38
|
+
"""Record a file that failed to delete."""
|
|
39
|
+
self.failed_count += 1
|
|
40
|
+
self.files_failed.append((path, error))
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def freed_mb(self) -> float:
|
|
44
|
+
"""Get freed space in MB."""
|
|
45
|
+
return self.total_freed_bytes / (1024 * 1024)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BackupCleanupService:
|
|
49
|
+
"""Cleans up old backup files based on age and count."""
|
|
50
|
+
|
|
51
|
+
def cleanup_backups(
|
|
52
|
+
self,
|
|
53
|
+
roadmap_dir: Path,
|
|
54
|
+
keep: int = 10,
|
|
55
|
+
days: int | None = None,
|
|
56
|
+
dry_run: bool = False,
|
|
57
|
+
) -> BackupCleanupResult:
|
|
58
|
+
"""Clean up old backup files.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
roadmap_dir: Roadmap directory
|
|
62
|
+
keep: Number of most recent backups to keep per issue
|
|
63
|
+
days: Delete backups older than this many days
|
|
64
|
+
dry_run: If True, don't delete files
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
BackupCleanupResult with deletion statistics
|
|
68
|
+
"""
|
|
69
|
+
backups_dir = roadmap_dir / "backups"
|
|
70
|
+
result = BackupCleanupResult()
|
|
71
|
+
|
|
72
|
+
if not backups_dir.exists():
|
|
73
|
+
logger.info("No backups directory found")
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
# Get files to delete
|
|
77
|
+
files_to_delete = self._select_backups_for_deletion(backups_dir, keep, days)
|
|
78
|
+
|
|
79
|
+
if not files_to_delete:
|
|
80
|
+
logger.info("No backups to clean up")
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
# Dry run: just record what would be deleted
|
|
84
|
+
if dry_run:
|
|
85
|
+
for backup in files_to_delete:
|
|
86
|
+
result.add_deleted(backup["path"], backup["size"])
|
|
87
|
+
logger.info(
|
|
88
|
+
"Dry run: would delete backups",
|
|
89
|
+
count=len(files_to_delete),
|
|
90
|
+
freed_mb=result.freed_mb,
|
|
91
|
+
)
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
# Actually delete files
|
|
95
|
+
for backup in files_to_delete:
|
|
96
|
+
try:
|
|
97
|
+
backup["path"].unlink()
|
|
98
|
+
result.add_deleted(backup["path"], backup["size"])
|
|
99
|
+
logger.info("Deleted backup file", file=backup["path"].name)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
result.add_failed(backup["path"], str(e))
|
|
102
|
+
logger.warning(
|
|
103
|
+
"Failed to delete backup",
|
|
104
|
+
file=backup["path"].name,
|
|
105
|
+
error=str(e),
|
|
106
|
+
severity="operational",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _group_backups_by_issue(
|
|
114
|
+
backup_files: list[Path],
|
|
115
|
+
) -> dict[str, list[BackupInfo]]:
|
|
116
|
+
"""Group backup files by issue ID.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
backup_files: List of backup file paths
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dict of issue_key -> list of BackupInfo
|
|
123
|
+
"""
|
|
124
|
+
backups_by_issue: dict[str, list[BackupInfo]] = {}
|
|
125
|
+
for backup_file in backup_files:
|
|
126
|
+
parts = backup_file.stem.split("_")
|
|
127
|
+
issue_key = "_".join(parts[:-1])
|
|
128
|
+
|
|
129
|
+
if issue_key not in backups_by_issue:
|
|
130
|
+
backups_by_issue[issue_key] = []
|
|
131
|
+
|
|
132
|
+
stat = backup_file.stat()
|
|
133
|
+
backups_by_issue[issue_key].append(
|
|
134
|
+
{
|
|
135
|
+
"path": backup_file,
|
|
136
|
+
"mtime": datetime.fromtimestamp(stat.st_mtime, tz=UTC),
|
|
137
|
+
"size": stat.st_size,
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return backups_by_issue
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _should_delete_backup(
|
|
145
|
+
idx: int, backup: BackupInfo, keep: int, cutoff_date: datetime | None
|
|
146
|
+
) -> bool:
|
|
147
|
+
"""Determine if backup should be deleted.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
idx: Index in sorted backup list
|
|
151
|
+
backup: Backup info dict
|
|
152
|
+
keep: Keep this many most recent backups
|
|
153
|
+
cutoff_date: Delete backups older than this date
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
True if backup should be deleted
|
|
157
|
+
"""
|
|
158
|
+
# Delete if beyond keep count
|
|
159
|
+
if idx >= keep:
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
# Delete if older than cutoff
|
|
163
|
+
if cutoff_date and backup["mtime"] < cutoff_date:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _select_backups_for_deletion(
|
|
170
|
+
backups_dir: Path, keep: int, days: int | None
|
|
171
|
+
) -> list[BackupInfo]:
|
|
172
|
+
"""Select backup files to delete based on age and count.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
backups_dir: Directory containing backup files
|
|
176
|
+
keep: Keep this many most recent backups per issue
|
|
177
|
+
days: Delete backups older than this many days
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of BackupInfo dicts for files to delete
|
|
181
|
+
"""
|
|
182
|
+
backup_files = list(backups_dir.glob("*.backup.md"))
|
|
183
|
+
if not backup_files:
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
# Group backups by issue ID
|
|
187
|
+
backups_by_issue = BackupCleanupService._group_backups_by_issue(backup_files)
|
|
188
|
+
|
|
189
|
+
# Determine files to delete
|
|
190
|
+
files_to_delete: list[BackupInfo] = []
|
|
191
|
+
cutoff_date = datetime.now(UTC) - timedelta(days=days) if days else None
|
|
192
|
+
|
|
193
|
+
for _issue_key, backups in backups_by_issue.items():
|
|
194
|
+
backups.sort(key=lambda x: x["mtime"], reverse=True)
|
|
195
|
+
|
|
196
|
+
for idx, backup in enumerate(backups):
|
|
197
|
+
if BackupCleanupService._should_delete_backup(
|
|
198
|
+
idx, backup, keep, cutoff_date
|
|
199
|
+
):
|
|
200
|
+
files_to_delete.append(backup)
|
|
201
|
+
|
|
202
|
+
return files_to_delete
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Data integrity validator service orchestrator.
|
|
2
|
+
|
|
3
|
+
Coordinates all data integrity validators and provides a unified interface
|
|
4
|
+
for running health checks across the system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from roadmap.common.logging import get_logger
|
|
8
|
+
from roadmap.core.services.validator_base import HealthStatus
|
|
9
|
+
from roadmap.core.services.validators import (
|
|
10
|
+
ArchivableIssuesValidator,
|
|
11
|
+
ArchivableMilestonesValidator,
|
|
12
|
+
BackupValidator,
|
|
13
|
+
DataIntegrityValidator,
|
|
14
|
+
DuplicateIssuesValidator,
|
|
15
|
+
DuplicateMilestonesValidator,
|
|
16
|
+
FolderStructureValidator,
|
|
17
|
+
OrphanedIssuesValidator,
|
|
18
|
+
OrphanedMilestonesValidator,
|
|
19
|
+
)
|
|
20
|
+
from roadmap.core.services.validators import (
|
|
21
|
+
__all__ as _validators_all,
|
|
22
|
+
)
|
|
23
|
+
from roadmap.core.services.validators.health_status_utils import (
|
|
24
|
+
get_overall_status,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
# Re-export validators for backward compatibility - use same list as validators module
|
|
30
|
+
_exported_validators: list[str] = (
|
|
31
|
+
list(_validators_all) if isinstance(_validators_all, list | tuple) else []
|
|
32
|
+
)
|
|
33
|
+
__all__ = ["DataIntegrityValidatorService"] + _exported_validators # type: ignore[operator]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DataIntegrityValidatorService:
|
|
37
|
+
"""Orchestrator for data integrity validation checks."""
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
"""Initialize data integrity validator service."""
|
|
41
|
+
# Note: This service uses static methods from validators.
|
|
42
|
+
# Instance creation is kept for potential future stateful functionality.
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def run_all_data_integrity_checks(self, core) -> dict[str, tuple[str, str]]:
|
|
46
|
+
"""Run all data integrity checks.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary mapping check names to (status, message) tuples
|
|
50
|
+
"""
|
|
51
|
+
checks = {}
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
checks["duplicate_issues"] = DuplicateIssuesValidator.perform_check()
|
|
55
|
+
checks["duplicate_milestones"] = (
|
|
56
|
+
DuplicateMilestonesValidator.perform_check()
|
|
57
|
+
)
|
|
58
|
+
checks["folder_structure"] = FolderStructureValidator.perform_check()
|
|
59
|
+
checks["old_backups"] = BackupValidator.check_old_backups()
|
|
60
|
+
checks["archivable_issues"] = (
|
|
61
|
+
ArchivableIssuesValidator.check_archivable_issues(core)
|
|
62
|
+
)
|
|
63
|
+
checks["archivable_milestones"] = (
|
|
64
|
+
ArchivableMilestonesValidator.check_archivable_milestones(core)
|
|
65
|
+
)
|
|
66
|
+
checks["data_integrity"] = DataIntegrityValidator.check_data_integrity()
|
|
67
|
+
checks["orphaned_issues"] = OrphanedIssuesValidator.check_orphaned_issues(
|
|
68
|
+
core
|
|
69
|
+
)
|
|
70
|
+
checks["orphaned_milestones"] = OrphanedMilestonesValidator.perform_check()
|
|
71
|
+
|
|
72
|
+
return checks
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(
|
|
75
|
+
"data_integrity_validation_failed", error=str(e), severity="data_error"
|
|
76
|
+
)
|
|
77
|
+
return {
|
|
78
|
+
"error": (
|
|
79
|
+
HealthStatus.UNHEALTHY,
|
|
80
|
+
f"Data integrity validation failed: {e}",
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def get_overall_status(self, checks: dict[str, tuple[str, str]]) -> str:
|
|
85
|
+
"""Get overall status from all data integrity checks.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Overall status: 'healthy', 'degraded', or 'unhealthy'
|
|
89
|
+
"""
|
|
90
|
+
return get_overall_status(checks)
|