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,309 @@
|
|
|
1
|
+
"""Service for issue creation operations.
|
|
2
|
+
|
|
3
|
+
Consolidates business logic for creating issues, including assignee resolution,
|
|
4
|
+
Git branch creation, and formatted display of created issues.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import structlog
|
|
13
|
+
|
|
14
|
+
from roadmap.common.console import get_console
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IssueCreationService:
|
|
20
|
+
"""Service for creating issues with all supporting operations."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, core):
|
|
23
|
+
"""Initialize service with core instance.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
core: RoadmapCore instance
|
|
27
|
+
"""
|
|
28
|
+
self.core = core
|
|
29
|
+
self._console = get_console()
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
32
|
+
# Assignee Resolution
|
|
33
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
def resolve_and_validate_assignee(
|
|
36
|
+
self, assignee: str | None = None, auto_detect: bool = True
|
|
37
|
+
) -> str | None:
|
|
38
|
+
"""Resolve and validate an assignee.
|
|
39
|
+
|
|
40
|
+
Auto-detects from Git if not provided. Validates against team members
|
|
41
|
+
and returns canonical name.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
assignee: Assignee name to resolve (optional)
|
|
45
|
+
auto_detect: Whether to auto-detect from Git if not provided
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Canonical assignee name or None if not assigned
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
click.Abort: If assignee validation fails
|
|
52
|
+
"""
|
|
53
|
+
# Track whether assignee was explicitly provided (for validation logic)
|
|
54
|
+
explicitly_provided = assignee is not None
|
|
55
|
+
|
|
56
|
+
# Auto-detect from Git if not provided
|
|
57
|
+
if not assignee and auto_detect:
|
|
58
|
+
git_user = self.core.git.get_current_user()
|
|
59
|
+
if git_user:
|
|
60
|
+
self._console.print(
|
|
61
|
+
f"🔍 Auto-detected assignee from Git: {git_user}", style="dim"
|
|
62
|
+
)
|
|
63
|
+
assignee = git_user
|
|
64
|
+
|
|
65
|
+
if not assignee:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Only validate if explicitly provided (not auto-detected)
|
|
69
|
+
if explicitly_provided:
|
|
70
|
+
is_valid, result = self.core.team.validate_assignee(assignee)
|
|
71
|
+
if not is_valid:
|
|
72
|
+
self._console.print(f"[ERROR] Invalid assignee: {result}")
|
|
73
|
+
raise click.Abort()
|
|
74
|
+
|
|
75
|
+
if result and "Warning:" in result:
|
|
76
|
+
self._console.print(f"⚠️ {result}", style="bold yellow")
|
|
77
|
+
return assignee
|
|
78
|
+
|
|
79
|
+
# Get canonical name
|
|
80
|
+
canonical = self.core.team.get_canonical_assignee(assignee)
|
|
81
|
+
if canonical != assignee:
|
|
82
|
+
self._console.print(
|
|
83
|
+
f"🔄 Resolved '{assignee}' to '{canonical}'", style="dim"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return canonical
|
|
87
|
+
else:
|
|
88
|
+
# Auto-detected assignee - use as-is without validation
|
|
89
|
+
return assignee
|
|
90
|
+
|
|
91
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
92
|
+
# Git Branch Creation
|
|
93
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def create_branch_for_issue(
|
|
96
|
+
self,
|
|
97
|
+
issue,
|
|
98
|
+
branch_name: str | None = None,
|
|
99
|
+
checkout: bool = True,
|
|
100
|
+
force: bool = False,
|
|
101
|
+
) -> tuple[bool, str | None]:
|
|
102
|
+
"""Create a Git branch for an issue.
|
|
103
|
+
|
|
104
|
+
Attempts multiple strategies to create and checkout the branch, with
|
|
105
|
+
fallbacks for various failure scenarios.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
issue: Issue object to create branch for
|
|
109
|
+
branch_name: Explicit branch name (optional)
|
|
110
|
+
checkout: Whether to checkout the branch
|
|
111
|
+
force: Whether to force override uncommitted changes
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Tuple of (success: bool, branch_name: Optional[str])
|
|
115
|
+
"""
|
|
116
|
+
if not hasattr(self.core, "git") or not self.core.git.is_git_repository():
|
|
117
|
+
self._console.print(
|
|
118
|
+
"⚠️ Not in a Git repository, skipping branch creation", style="yellow"
|
|
119
|
+
)
|
|
120
|
+
return False, None
|
|
121
|
+
|
|
122
|
+
# Determine branch name
|
|
123
|
+
resolved_name = branch_name or self.core.git.suggest_branch_name(issue.id)
|
|
124
|
+
|
|
125
|
+
# Try primary method
|
|
126
|
+
if self._try_safe_create_branch(issue, checkout, force):
|
|
127
|
+
self._show_branch_success_message(resolved_name, checkout)
|
|
128
|
+
return True, resolved_name
|
|
129
|
+
|
|
130
|
+
# Check for uncommitted changes
|
|
131
|
+
if self._has_uncommitted_changes():
|
|
132
|
+
self._console.print(
|
|
133
|
+
"⚠️ Working tree has uncommitted changes — branch creation skipped. Use --force to override.",
|
|
134
|
+
style="yellow",
|
|
135
|
+
)
|
|
136
|
+
return False, resolved_name
|
|
137
|
+
|
|
138
|
+
# Try fallback strategies
|
|
139
|
+
if self._try_direct_git_command(resolved_name, checkout):
|
|
140
|
+
return True, resolved_name
|
|
141
|
+
|
|
142
|
+
if self._try_subprocess_git(resolved_name, checkout):
|
|
143
|
+
return True, resolved_name
|
|
144
|
+
|
|
145
|
+
# All strategies failed
|
|
146
|
+
self._console.print(
|
|
147
|
+
"⚠️ Failed to create or checkout branch. See git for details.",
|
|
148
|
+
style="yellow",
|
|
149
|
+
)
|
|
150
|
+
return False, resolved_name
|
|
151
|
+
|
|
152
|
+
def _try_safe_create_branch(self, issue, checkout: bool, force: bool) -> bool:
|
|
153
|
+
"""Try creating branch using core.git method."""
|
|
154
|
+
try:
|
|
155
|
+
return self.core.git.create_branch_for_issue(
|
|
156
|
+
issue, checkout=checkout, force=force
|
|
157
|
+
)
|
|
158
|
+
except TypeError:
|
|
159
|
+
# Try without force parameter
|
|
160
|
+
try:
|
|
161
|
+
return self.core.git.create_branch_for_issue(issue, checkout=checkout)
|
|
162
|
+
except TypeError:
|
|
163
|
+
# Try with just issue
|
|
164
|
+
try:
|
|
165
|
+
return self.core.git.create_branch_for_issue(issue)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.debug(
|
|
168
|
+
"create_branch_fallback_failed",
|
|
169
|
+
operation="create_branch_for_issue",
|
|
170
|
+
error=str(e),
|
|
171
|
+
action="Returning False",
|
|
172
|
+
)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def _has_uncommitted_changes(self) -> bool:
|
|
176
|
+
"""Check if there are uncommitted changes."""
|
|
177
|
+
try:
|
|
178
|
+
status_output = (
|
|
179
|
+
self.core.git._run_git_command(["status", "--porcelain"]) or ""
|
|
180
|
+
)
|
|
181
|
+
return bool(status_output.strip())
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.debug(
|
|
184
|
+
"git_status_check_failed",
|
|
185
|
+
operation="check_git_status",
|
|
186
|
+
error=str(e),
|
|
187
|
+
action="Assuming no changes",
|
|
188
|
+
)
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _try_direct_git_command(self, branch_name: str, checkout: bool) -> bool:
|
|
192
|
+
"""Try creating branch using direct git command via core.git."""
|
|
193
|
+
try:
|
|
194
|
+
result = self.core.git._run_git_command(["checkout", "-b", branch_name])
|
|
195
|
+
if result is not None:
|
|
196
|
+
self._show_branch_success_message(branch_name, checkout)
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
# Check if branch exists
|
|
200
|
+
exists = self.core.git._run_git_command(
|
|
201
|
+
["rev-parse", "--verify", branch_name]
|
|
202
|
+
)
|
|
203
|
+
if exists:
|
|
204
|
+
self._show_branch_success_message(branch_name, checkout)
|
|
205
|
+
return True
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.debug(
|
|
208
|
+
"direct_git_command_failed",
|
|
209
|
+
operation="try_direct_git_command",
|
|
210
|
+
branch=branch_name,
|
|
211
|
+
error=str(e),
|
|
212
|
+
action="Falling back to subprocess",
|
|
213
|
+
)
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def _try_subprocess_git(self, branch_name: str, checkout: bool) -> bool:
|
|
217
|
+
"""Try creating branch using subprocess as last resort."""
|
|
218
|
+
try:
|
|
219
|
+
cwd = getattr(self.core, "root_path", None) or os.getcwd()
|
|
220
|
+
subprocess.run(
|
|
221
|
+
["git", "checkout", "-b", branch_name],
|
|
222
|
+
cwd=cwd,
|
|
223
|
+
check=True,
|
|
224
|
+
capture_output=True,
|
|
225
|
+
text=True,
|
|
226
|
+
)
|
|
227
|
+
self._show_branch_success_message(branch_name, checkout)
|
|
228
|
+
return True
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.debug(
|
|
231
|
+
"branch_creation_failed",
|
|
232
|
+
branch_name=branch_name,
|
|
233
|
+
error=str(e),
|
|
234
|
+
action="create_branch",
|
|
235
|
+
)
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def _show_branch_success_message(self, branch_name: str, checkout: bool) -> None:
|
|
239
|
+
"""Display success message for branch creation."""
|
|
240
|
+
self._console.print(f"🌿 Created Git branch: {branch_name}", style="green")
|
|
241
|
+
if checkout:
|
|
242
|
+
self._console.print(f"✅ Checked out branch: {branch_name}", style="green")
|
|
243
|
+
|
|
244
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
245
|
+
# Display Formatting
|
|
246
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def format_created_issue_display(self, issue, milestone: str | None = None) -> None:
|
|
249
|
+
"""Display formatted information about a newly created issue.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
issue: Created issue object
|
|
253
|
+
milestone: Optional milestone name
|
|
254
|
+
"""
|
|
255
|
+
self._console.print(f"✅ Created issue: {issue.title}", style="bold green")
|
|
256
|
+
self._console.print(f" ID: {issue.id}", style="bold cyan")
|
|
257
|
+
self._console.print(f" Type: {issue.issue_type.value.title()}", style="blue")
|
|
258
|
+
self._console.print(f" Priority: {issue.priority.value}", style="yellow")
|
|
259
|
+
|
|
260
|
+
if milestone:
|
|
261
|
+
self._console.print(f" Milestone: {milestone}", style="blue")
|
|
262
|
+
|
|
263
|
+
if self._should_show_assignee(issue.assignee):
|
|
264
|
+
self._console.print(f" Assignee: {issue.assignee}", style="magenta")
|
|
265
|
+
|
|
266
|
+
self._print_estimate_if_present(issue)
|
|
267
|
+
self._print_dependency_list(
|
|
268
|
+
issue, field_name="depends_on", label="Depends on", style="orange1"
|
|
269
|
+
)
|
|
270
|
+
self._print_dependency_list(
|
|
271
|
+
issue, field_name="blocks", label="Blocks", style="red1"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if hasattr(issue, "filename"):
|
|
275
|
+
self._console.print(
|
|
276
|
+
f" File: .roadmap/issues/{issue.filename}", style="dim"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _should_show_assignee(self, assignee: object) -> bool:
|
|
280
|
+
"""Return True when assignee should be displayed in success output."""
|
|
281
|
+
if not assignee:
|
|
282
|
+
return False
|
|
283
|
+
assignee_text = str(assignee)
|
|
284
|
+
return "<Mock" not in assignee_text
|
|
285
|
+
|
|
286
|
+
def _print_estimate_if_present(self, issue: Any) -> None:
|
|
287
|
+
"""Print estimated effort if issue contains estimate information."""
|
|
288
|
+
if hasattr(issue, "estimated_hours") and issue.estimated_hours:
|
|
289
|
+
self._console.print(
|
|
290
|
+
f" Estimated: {issue.estimated_time_display}", style="green"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _print_dependency_list(
|
|
294
|
+
self,
|
|
295
|
+
issue: Any,
|
|
296
|
+
*,
|
|
297
|
+
field_name: str,
|
|
298
|
+
label: str,
|
|
299
|
+
style: str,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Print a dependency list field when present and well-formed."""
|
|
302
|
+
values = getattr(issue, field_name, None)
|
|
303
|
+
if not values:
|
|
304
|
+
return
|
|
305
|
+
try:
|
|
306
|
+
if isinstance(values, list | tuple):
|
|
307
|
+
self._console.print(f" {label}: {', '.join(values)}", style=style)
|
|
308
|
+
except (TypeError, AttributeError):
|
|
309
|
+
return
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Issue filtering service for issue queries and filtering.
|
|
2
|
+
|
|
3
|
+
Provides utilities for filtering issues based on various criteria and calculating workload.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import UTC
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from roadmap.core.domain import Issue, Priority, Status
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore # noqa: F401
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IssueFilterValidator:
|
|
16
|
+
"""Validates filter combinations for issue queries."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def validate_filters(
|
|
20
|
+
assignee: str | None,
|
|
21
|
+
my_issues: bool,
|
|
22
|
+
backlog: bool,
|
|
23
|
+
unassigned: bool,
|
|
24
|
+
next_milestone: bool,
|
|
25
|
+
milestone: str | None,
|
|
26
|
+
) -> tuple[bool, str | None]:
|
|
27
|
+
"""Validate that filter combinations are valid.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple of (is_valid, error_message)
|
|
31
|
+
"""
|
|
32
|
+
# Check for conflicting assignee filters
|
|
33
|
+
assignee_filters = [assignee is not None, my_issues]
|
|
34
|
+
if sum(bool(f) for f in assignee_filters) > 1:
|
|
35
|
+
return False, "Cannot combine --assignee and --my-issues filters"
|
|
36
|
+
|
|
37
|
+
# Check for conflicting milestone filters
|
|
38
|
+
exclusive_filters = [backlog, unassigned, next_milestone, milestone is not None]
|
|
39
|
+
if sum(bool(f) for f in exclusive_filters) > 1:
|
|
40
|
+
return (
|
|
41
|
+
False,
|
|
42
|
+
"Cannot combine --backlog, --unassigned, --next-milestone, and --milestone filters",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return True, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class IssueQueryService:
|
|
49
|
+
"""Service for querying issues with different filter criteria."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, core: "RoadmapCore"): # noqa: F821
|
|
52
|
+
"""Initialize IssueQueryService.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
core: Core roadmap instance.
|
|
56
|
+
"""
|
|
57
|
+
self.core = core
|
|
58
|
+
|
|
59
|
+
def _get_issues_for_assignee_filters(
|
|
60
|
+
self, my_issues: bool, assignee: str | None
|
|
61
|
+
) -> tuple[list[Issue], str] | None:
|
|
62
|
+
"""Get issues for assignee-based filters. Returns None if no match."""
|
|
63
|
+
if my_issues:
|
|
64
|
+
return self.core.team.get_my_issues(), "my"
|
|
65
|
+
if assignee:
|
|
66
|
+
return self.core.team.get_assigned_issues(
|
|
67
|
+
assignee
|
|
68
|
+
), f"assigned to {assignee}"
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
def _get_issues_for_collection_filters(
|
|
72
|
+
self,
|
|
73
|
+
backlog: bool,
|
|
74
|
+
unassigned: bool,
|
|
75
|
+
next_milestone: bool,
|
|
76
|
+
milestone: str | None,
|
|
77
|
+
) -> tuple[list[Issue], str] | None:
|
|
78
|
+
"""Get issues for collection-based filters. Returns None if no match."""
|
|
79
|
+
if backlog or unassigned:
|
|
80
|
+
return self.core.issues.get_backlog(), "backlog"
|
|
81
|
+
|
|
82
|
+
if next_milestone:
|
|
83
|
+
next_ms = self.core.milestones.get_next()
|
|
84
|
+
if not next_ms:
|
|
85
|
+
return [], "" # Handled by caller
|
|
86
|
+
return (
|
|
87
|
+
self.core.issues.get_by_milestone(next_ms.name),
|
|
88
|
+
f"next milestone ({next_ms.name})",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if milestone:
|
|
92
|
+
return (
|
|
93
|
+
self.core.issues.get_by_milestone(milestone),
|
|
94
|
+
f"milestone '{milestone}'",
|
|
95
|
+
)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def _get_overdue_issues(self) -> tuple[list[Issue], str]:
|
|
99
|
+
"""Get overdue issues."""
|
|
100
|
+
from datetime import datetime
|
|
101
|
+
|
|
102
|
+
all_issues = self.core.issues.list()
|
|
103
|
+
now = datetime.now(UTC)
|
|
104
|
+
overdue_issues: list[Issue] = []
|
|
105
|
+
for issue in all_issues:
|
|
106
|
+
if not issue.due_date:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
due = issue.due_date
|
|
110
|
+
if isinstance(due, datetime):
|
|
111
|
+
if due.tzinfo:
|
|
112
|
+
due_aware = due.astimezone(UTC)
|
|
113
|
+
else:
|
|
114
|
+
due_aware = due.replace(tzinfo=UTC)
|
|
115
|
+
else:
|
|
116
|
+
due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
|
|
117
|
+
|
|
118
|
+
if due_aware < now:
|
|
119
|
+
overdue_issues.append(issue)
|
|
120
|
+
|
|
121
|
+
return overdue_issues, "overdue"
|
|
122
|
+
|
|
123
|
+
def get_filtered_issues(
|
|
124
|
+
self,
|
|
125
|
+
milestone: str | None = None,
|
|
126
|
+
backlog: bool = False,
|
|
127
|
+
unassigned: bool = False,
|
|
128
|
+
next_milestone: bool = False,
|
|
129
|
+
assignee: str | None = None,
|
|
130
|
+
my_issues: bool = False,
|
|
131
|
+
overdue: bool = False,
|
|
132
|
+
) -> tuple[list[Issue], str]:
|
|
133
|
+
"""Get issues based on primary filter criteria.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Tuple of (issues, filter_description)
|
|
137
|
+
"""
|
|
138
|
+
# Try assignee filters first
|
|
139
|
+
result = self._get_issues_for_assignee_filters(my_issues, assignee)
|
|
140
|
+
if result is not None:
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
# Try collection filters
|
|
144
|
+
result = self._get_issues_for_collection_filters(
|
|
145
|
+
backlog, unassigned, next_milestone, milestone
|
|
146
|
+
)
|
|
147
|
+
if result is not None:
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
# Handle overdue filter
|
|
151
|
+
if overdue:
|
|
152
|
+
return self._get_overdue_issues()
|
|
153
|
+
|
|
154
|
+
# Show all issues
|
|
155
|
+
return self.core.issues.list(), "all"
|
|
156
|
+
|
|
157
|
+
def apply_additional_filters(
|
|
158
|
+
self,
|
|
159
|
+
issues: list[Issue],
|
|
160
|
+
filter_description: str,
|
|
161
|
+
open_only: bool = False,
|
|
162
|
+
blocked_only: bool = False,
|
|
163
|
+
status: str | None = None,
|
|
164
|
+
priority: str | None = None,
|
|
165
|
+
issue_type: str | None = None,
|
|
166
|
+
overdue: bool = False,
|
|
167
|
+
) -> tuple[list[Issue], str]:
|
|
168
|
+
"""Apply additional filters to issue list.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Tuple of (filtered_issues, updated_description)
|
|
172
|
+
"""
|
|
173
|
+
result = issues
|
|
174
|
+
description = filter_description
|
|
175
|
+
|
|
176
|
+
# Apply each filter in sequence
|
|
177
|
+
if open_only:
|
|
178
|
+
result, description = self._filter_by_open_status(result, description)
|
|
179
|
+
|
|
180
|
+
if blocked_only:
|
|
181
|
+
result, description = self._filter_by_blocked_status(result, description)
|
|
182
|
+
|
|
183
|
+
if status:
|
|
184
|
+
result, description = self._filter_by_status(result, description, status)
|
|
185
|
+
|
|
186
|
+
if priority:
|
|
187
|
+
result, description = self._filter_by_priority(
|
|
188
|
+
result, description, priority
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if issue_type:
|
|
192
|
+
result, description = self._filter_by_type(result, description, issue_type)
|
|
193
|
+
|
|
194
|
+
if overdue:
|
|
195
|
+
result, description = self._filter_by_overdue(result, description)
|
|
196
|
+
|
|
197
|
+
return result, description
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _filter_by_open_status(
|
|
201
|
+
issues: list[Issue], description: str
|
|
202
|
+
) -> tuple[list[Issue], str]:
|
|
203
|
+
"""Filter to only open issues (not closed)."""
|
|
204
|
+
filtered = [i for i in issues if i.status != Status.CLOSED]
|
|
205
|
+
return filtered, description + " open"
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def _filter_by_blocked_status(
|
|
209
|
+
issues: list[Issue], description: str
|
|
210
|
+
) -> tuple[list[Issue], str]:
|
|
211
|
+
"""Filter to only blocked issues."""
|
|
212
|
+
filtered = [i for i in issues if i.status == Status.BLOCKED]
|
|
213
|
+
return filtered, description + " blocked"
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _filter_by_status(
|
|
217
|
+
issues: list[Issue], description: str, status: str
|
|
218
|
+
) -> tuple[list[Issue], str]:
|
|
219
|
+
"""Filter by specific status."""
|
|
220
|
+
filtered = [i for i in issues if i.status == Status(status)]
|
|
221
|
+
return filtered, description + f" {status}"
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _filter_by_priority(
|
|
225
|
+
issues: list[Issue], description: str, priority: str
|
|
226
|
+
) -> tuple[list[Issue], str]:
|
|
227
|
+
"""Filter by priority level."""
|
|
228
|
+
filtered = [i for i in issues if i.priority == Priority(priority)]
|
|
229
|
+
return filtered, description + f" {priority} priority"
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def _filter_by_type(
|
|
233
|
+
issues: list[Issue], description: str, issue_type: str
|
|
234
|
+
) -> tuple[list[Issue], str]:
|
|
235
|
+
"""Filter by issue type."""
|
|
236
|
+
from roadmap.core.domain import IssueType
|
|
237
|
+
|
|
238
|
+
filtered = [i for i in issues if i.issue_type == IssueType(issue_type)]
|
|
239
|
+
return filtered, description + f" {issue_type}"
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _filter_by_overdue(
|
|
243
|
+
issues: list[Issue], description: str
|
|
244
|
+
) -> tuple[list[Issue], str]:
|
|
245
|
+
"""Filter to only overdue issues."""
|
|
246
|
+
from datetime import datetime
|
|
247
|
+
|
|
248
|
+
now = datetime.now(UTC)
|
|
249
|
+
filtered: list[Issue] = []
|
|
250
|
+
for i in issues:
|
|
251
|
+
if not i.due_date:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
due = i.due_date
|
|
255
|
+
if isinstance(due, datetime):
|
|
256
|
+
if due.tzinfo:
|
|
257
|
+
due_aware = due.astimezone(UTC)
|
|
258
|
+
else:
|
|
259
|
+
due_aware = due.replace(tzinfo=UTC)
|
|
260
|
+
else:
|
|
261
|
+
due_aware = datetime.fromisoformat(str(due)).replace(tzinfo=UTC)
|
|
262
|
+
|
|
263
|
+
if due_aware < now:
|
|
264
|
+
filtered.append(i)
|
|
265
|
+
|
|
266
|
+
return filtered, description + " overdue"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class WorkloadCalculator:
|
|
270
|
+
"""Calculates time estimates and workload breakdowns."""
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def calculate_workload(issues: list[Issue]) -> dict:
|
|
274
|
+
"""Calculate workload statistics for a list of issues.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dictionary with total_hours, status_breakdown
|
|
278
|
+
"""
|
|
279
|
+
total_hours = sum(issue.estimated_hours or 0 for issue in issues)
|
|
280
|
+
|
|
281
|
+
status_counts = {}
|
|
282
|
+
for issue in issues:
|
|
283
|
+
status = issue.status.value
|
|
284
|
+
if status not in status_counts:
|
|
285
|
+
status_counts[status] = {"count": 0, "hours": 0}
|
|
286
|
+
status_counts[status]["count"] += 1
|
|
287
|
+
status_counts[status]["hours"] += issue.estimated_hours or 0
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
"total_hours": total_hours,
|
|
291
|
+
"status_breakdown": status_counts,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def format_time_display(hours: float) -> str:
|
|
296
|
+
"""Format time in hours to human-readable display."""
|
|
297
|
+
if hours < 1:
|
|
298
|
+
return f"{hours * 60:.0f}m"
|
|
299
|
+
elif hours <= 24:
|
|
300
|
+
return f"{hours:.1f}h"
|
|
301
|
+
else:
|
|
302
|
+
return f"{hours / 8:.1f}d"
|