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,381 @@
|
|
|
1
|
+
"""Output abstraction layer - Core data structures for structured output.
|
|
2
|
+
|
|
3
|
+
This module provides the foundation for multi-format output (Rich, plain-text, JSON, CSV).
|
|
4
|
+
Commands return structured TableData objects instead of formatted strings, allowing
|
|
5
|
+
the same data to be rendered in multiple formats.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ColumnType(StrEnum):
|
|
14
|
+
"""Supported column data types."""
|
|
15
|
+
|
|
16
|
+
STRING = "string"
|
|
17
|
+
INTEGER = "integer"
|
|
18
|
+
FLOAT = "float"
|
|
19
|
+
BOOLEAN = "boolean"
|
|
20
|
+
DATE = "date"
|
|
21
|
+
DATETIME = "datetime"
|
|
22
|
+
ENUM = "enum"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ColumnDef:
|
|
27
|
+
"""Metadata for a single table column.
|
|
28
|
+
|
|
29
|
+
This separates column structure from presentation, enabling the same
|
|
30
|
+
column to be rendered in multiple output formats.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
name: Column identifier (e.g., "id", "title"). Used for filtering/sorting.
|
|
34
|
+
display_name: Human-readable header (e.g., "ID", "Title").
|
|
35
|
+
type: Column data type (string, integer, date, enum, etc.).
|
|
36
|
+
width: Suggested display width in characters (for text-based formats).
|
|
37
|
+
headline: Help text describing the column purpose.
|
|
38
|
+
display_style: Rich styling (e.g., "cyan", "bold green"). Ignored in plain-text/JSON/CSV.
|
|
39
|
+
enum_values: Valid values for type="enum". None for other types.
|
|
40
|
+
sortable: Can users sort by this column?
|
|
41
|
+
filterable: Can users filter by this column?
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
ColumnDef(
|
|
45
|
+
name="status",
|
|
46
|
+
display_name="Status",
|
|
47
|
+
type=ColumnType.ENUM,
|
|
48
|
+
width=10,
|
|
49
|
+
headline="Issue status",
|
|
50
|
+
display_style="yellow",
|
|
51
|
+
enum_values=["open", "closed", "blocked"],
|
|
52
|
+
sortable=True,
|
|
53
|
+
filterable=True,
|
|
54
|
+
)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name: str
|
|
58
|
+
display_name: str
|
|
59
|
+
type: ColumnType = ColumnType.STRING
|
|
60
|
+
width: int | None = None
|
|
61
|
+
headline: str = ""
|
|
62
|
+
display_style: str | None = None
|
|
63
|
+
enum_values: list[str] | None = None
|
|
64
|
+
sortable: bool = True
|
|
65
|
+
filterable: bool = True
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict[str, Any]:
|
|
68
|
+
"""Export column metadata as dictionary."""
|
|
69
|
+
return {
|
|
70
|
+
"name": self.name,
|
|
71
|
+
"display_name": self.display_name,
|
|
72
|
+
"type": self.type.value,
|
|
73
|
+
"width": self.width,
|
|
74
|
+
"headline": self.headline,
|
|
75
|
+
"display_style": self.display_style,
|
|
76
|
+
"enum_values": self.enum_values,
|
|
77
|
+
"sortable": self.sortable,
|
|
78
|
+
"filterable": self.filterable,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def from_dict(data: dict[str, Any]) -> "ColumnDef":
|
|
83
|
+
"""Import column metadata from dictionary."""
|
|
84
|
+
return ColumnDef(
|
|
85
|
+
name=data["name"],
|
|
86
|
+
display_name=data.get("display_name", data["name"]),
|
|
87
|
+
type=ColumnType(data.get("type", "string")),
|
|
88
|
+
width=data.get("width"),
|
|
89
|
+
headline=data.get("headline", ""),
|
|
90
|
+
display_style=data.get("display_style"),
|
|
91
|
+
enum_values=data.get("enum_values"),
|
|
92
|
+
sortable=data.get("sortable", True),
|
|
93
|
+
filterable=data.get("filterable", True),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class TableData:
|
|
99
|
+
"""Structured table data with metadata.
|
|
100
|
+
|
|
101
|
+
Contains the data and metadata needed to render a table in any format
|
|
102
|
+
(Rich, plain-text, JSON, CSV). This decouples data from presentation.
|
|
103
|
+
|
|
104
|
+
Attributes:
|
|
105
|
+
columns: List of ColumnDef objects defining table structure.
|
|
106
|
+
rows: List of rows, where each row is a list of values matching column order.
|
|
107
|
+
title: Optional table title/heading.
|
|
108
|
+
headline: Optional table description.
|
|
109
|
+
filters_applied: Dictionary of {column_name: filter_value} for active filters.
|
|
110
|
+
sort_by: List of (column_name, "asc"|"desc") tuples for sort order.
|
|
111
|
+
selected_columns: List of column names to display (None = all columns).
|
|
112
|
+
total_count: Total number of rows before filtering (for pagination).
|
|
113
|
+
returned_count: Number of rows returned after filtering.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
table = TableData(
|
|
117
|
+
columns=[
|
|
118
|
+
ColumnDef(name="id", display_name="ID", type=ColumnType.STRING, width=9),
|
|
119
|
+
ColumnDef(name="title", display_name="Title", type=ColumnType.STRING),
|
|
120
|
+
ColumnDef(name="status", display_name="Status", type=ColumnType.ENUM,
|
|
121
|
+
enum_values=["open", "closed"]),
|
|
122
|
+
],
|
|
123
|
+
rows=[
|
|
124
|
+
["123", "Fix bug", "open"],
|
|
125
|
+
["124", "Add feature", "closed"],
|
|
126
|
+
],
|
|
127
|
+
title="Issues",
|
|
128
|
+
)
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
columns: list[ColumnDef]
|
|
132
|
+
rows: list[list[Any]]
|
|
133
|
+
title: str | None = None
|
|
134
|
+
headline: str | None = None
|
|
135
|
+
filters_applied: dict[str, Any] = field(default_factory=dict)
|
|
136
|
+
sort_by: list[tuple[str, str]] | list = field(default_factory=list)
|
|
137
|
+
selected_columns: list[str] | None = None
|
|
138
|
+
total_count: int | None = None
|
|
139
|
+
returned_count: int | None = None
|
|
140
|
+
|
|
141
|
+
def __post_init__(self):
|
|
142
|
+
"""Validate table structure after initialization."""
|
|
143
|
+
# Validate row structure
|
|
144
|
+
if self.rows:
|
|
145
|
+
expected_columns = len(self.columns)
|
|
146
|
+
for i, row in enumerate(self.rows):
|
|
147
|
+
if len(row) != expected_columns:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Row {i} has {len(row)} values but {expected_columns} columns expected"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Set defaults for counts
|
|
153
|
+
if self.total_count is None:
|
|
154
|
+
self.total_count = len(self.rows)
|
|
155
|
+
if self.returned_count is None:
|
|
156
|
+
self.returned_count = len(self.rows)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def active_columns(self) -> list[ColumnDef]:
|
|
160
|
+
"""Get columns to display (respecting selected_columns)."""
|
|
161
|
+
if self.selected_columns:
|
|
162
|
+
# Return columns in selected order
|
|
163
|
+
col_map = {col.name: col for col in self.columns}
|
|
164
|
+
return [col_map[name] for name in self.selected_columns if name in col_map]
|
|
165
|
+
return self.columns
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def active_rows(self) -> list[list[Any]]:
|
|
169
|
+
"""Get rows with only selected columns."""
|
|
170
|
+
if not self.selected_columns:
|
|
171
|
+
return self.rows
|
|
172
|
+
|
|
173
|
+
# Build column index mapping
|
|
174
|
+
col_indices = {col.name: i for i, col in enumerate(self.columns)}
|
|
175
|
+
selected_indices = [
|
|
176
|
+
col_indices[name] for name in self.selected_columns if name in col_indices
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
# Filter row values
|
|
180
|
+
return [[row[i] for i in selected_indices] for row in self.rows]
|
|
181
|
+
|
|
182
|
+
def filter(self, column: str, value: Any) -> "TableData":
|
|
183
|
+
"""Apply filter and return new TableData.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
column: Column name to filter by.
|
|
187
|
+
value: Value(s) to filter for. Can be single value or list for IN filter.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
New TableData with filtered rows.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If column doesn't exist or value doesn't match enum values.
|
|
194
|
+
"""
|
|
195
|
+
col = self._get_filter_column(column)
|
|
196
|
+
self._validate_enum_filter_values(column, col, value)
|
|
197
|
+
col_index = self._get_column_index(column)
|
|
198
|
+
filtered_rows = self._filter_rows(column_index=col_index, value=value)
|
|
199
|
+
|
|
200
|
+
# Create new TableData with updated state
|
|
201
|
+
new_table = TableData(
|
|
202
|
+
columns=self.columns,
|
|
203
|
+
rows=filtered_rows,
|
|
204
|
+
title=self.title,
|
|
205
|
+
headline=self.headline,
|
|
206
|
+
filters_applied={**self.filters_applied, column: value},
|
|
207
|
+
sort_by=self.sort_by,
|
|
208
|
+
selected_columns=self.selected_columns,
|
|
209
|
+
total_count=self.total_count,
|
|
210
|
+
returned_count=len(filtered_rows),
|
|
211
|
+
)
|
|
212
|
+
return new_table
|
|
213
|
+
|
|
214
|
+
def _get_filter_column(self, column: str) -> ColumnDef:
|
|
215
|
+
"""Return filter column definition or raise a validation error."""
|
|
216
|
+
col = next((c for c in self.columns if c.name == column), None)
|
|
217
|
+
if not col:
|
|
218
|
+
raise ValueError(f"Column '{column}' not found")
|
|
219
|
+
if not col.filterable:
|
|
220
|
+
raise ValueError(f"Column '{column}' is not filterable")
|
|
221
|
+
return col
|
|
222
|
+
|
|
223
|
+
def _validate_enum_filter_values(
|
|
224
|
+
self,
|
|
225
|
+
column: str,
|
|
226
|
+
col: ColumnDef,
|
|
227
|
+
value: Any,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Validate enum filter values against allowed values."""
|
|
230
|
+
if col.type != ColumnType.ENUM or not col.enum_values:
|
|
231
|
+
return
|
|
232
|
+
values_to_check = value if isinstance(value, list) else [value]
|
|
233
|
+
for candidate in values_to_check:
|
|
234
|
+
if candidate not in col.enum_values:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Invalid value '{candidate}' for enum column '{column}'"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def _get_column_index(self, column: str) -> int:
|
|
240
|
+
"""Return index of the named column."""
|
|
241
|
+
return next(i for i, c in enumerate(self.columns) if c.name == column)
|
|
242
|
+
|
|
243
|
+
def _filter_rows(self, *, column_index: int, value: Any) -> list[list[Any]]:
|
|
244
|
+
"""Apply either equality or IN filtering to rows."""
|
|
245
|
+
if isinstance(value, list):
|
|
246
|
+
return [row for row in self.rows if row[column_index] in value]
|
|
247
|
+
return [row for row in self.rows if row[column_index] == value]
|
|
248
|
+
|
|
249
|
+
def sort(self, sort_spec: str | list[tuple[str, str]]) -> "TableData":
|
|
250
|
+
"""Apply sorting and return new TableData.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
sort_spec: Either a string (column name, ascending) or
|
|
254
|
+
list of (column_name, "asc"|"desc") tuples.
|
|
255
|
+
Sorts by first column, then second, etc.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
New TableData with sorted rows.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If column doesn't exist or is not sortable.
|
|
262
|
+
"""
|
|
263
|
+
# Normalize sort_spec to list of tuples
|
|
264
|
+
if isinstance(sort_spec, str):
|
|
265
|
+
sort_spec_list = [(sort_spec, "asc")]
|
|
266
|
+
else:
|
|
267
|
+
sort_spec_list = sort_spec
|
|
268
|
+
|
|
269
|
+
# Validate columns and build sort key function
|
|
270
|
+
col_indices = {}
|
|
271
|
+
sort_directions = {}
|
|
272
|
+
|
|
273
|
+
for col_name, direction in sort_spec_list:
|
|
274
|
+
col = next((c for c in self.columns if c.name == col_name), None)
|
|
275
|
+
if not col:
|
|
276
|
+
raise ValueError(f"Column '{col_name}' not found")
|
|
277
|
+
if not col.sortable:
|
|
278
|
+
raise ValueError(f"Column '{col_name}' is not sortable")
|
|
279
|
+
|
|
280
|
+
col_index = next(
|
|
281
|
+
i for i, c in enumerate(self.columns) if c.name == col_name
|
|
282
|
+
)
|
|
283
|
+
col_indices[col_name] = col_index
|
|
284
|
+
sort_directions[col_name] = direction.lower() == "desc"
|
|
285
|
+
|
|
286
|
+
# Sort rows using multiple keys
|
|
287
|
+
def sort_key(row):
|
|
288
|
+
keys = []
|
|
289
|
+
for col_name, _ in sort_spec_list:
|
|
290
|
+
index = col_indices[col_name]
|
|
291
|
+
value = row[index]
|
|
292
|
+
# Handle None values
|
|
293
|
+
if value is None:
|
|
294
|
+
keys.append((1, value)) # None sorts to end
|
|
295
|
+
else:
|
|
296
|
+
keys.append((0, value))
|
|
297
|
+
return tuple(keys)
|
|
298
|
+
|
|
299
|
+
sorted_rows = sorted(self.rows, key=sort_key)
|
|
300
|
+
|
|
301
|
+
# Apply descending direction
|
|
302
|
+
for col_name, _direction in reversed(sort_spec_list):
|
|
303
|
+
if sort_directions[col_name]:
|
|
304
|
+
sorted_rows.reverse()
|
|
305
|
+
|
|
306
|
+
# Create new TableData with updated state
|
|
307
|
+
new_table = TableData(
|
|
308
|
+
columns=self.columns,
|
|
309
|
+
rows=sorted_rows,
|
|
310
|
+
title=self.title,
|
|
311
|
+
headline=self.headline,
|
|
312
|
+
filters_applied=self.filters_applied,
|
|
313
|
+
sort_by=sort_spec_list,
|
|
314
|
+
selected_columns=self.selected_columns,
|
|
315
|
+
total_count=self.total_count,
|
|
316
|
+
returned_count=self.returned_count,
|
|
317
|
+
)
|
|
318
|
+
return new_table
|
|
319
|
+
|
|
320
|
+
def select_columns(self, columns: list[str]) -> "TableData":
|
|
321
|
+
"""Select specific columns to display.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
columns: List of column names to display.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
New TableData with column selection applied.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
ValueError: If any column name doesn't exist.
|
|
331
|
+
"""
|
|
332
|
+
# Validate all columns exist
|
|
333
|
+
col_names = {c.name for c in self.columns}
|
|
334
|
+
for col in columns:
|
|
335
|
+
if col not in col_names:
|
|
336
|
+
raise ValueError(f"Column '{col}' not found")
|
|
337
|
+
|
|
338
|
+
# Create new TableData with column selection
|
|
339
|
+
new_table = TableData(
|
|
340
|
+
columns=self.columns,
|
|
341
|
+
rows=self.rows,
|
|
342
|
+
title=self.title,
|
|
343
|
+
headline=self.headline,
|
|
344
|
+
filters_applied=self.filters_applied,
|
|
345
|
+
sort_by=self.sort_by,
|
|
346
|
+
selected_columns=columns,
|
|
347
|
+
total_count=self.total_count,
|
|
348
|
+
returned_count=self.returned_count,
|
|
349
|
+
)
|
|
350
|
+
return new_table
|
|
351
|
+
|
|
352
|
+
def to_dict(self) -> dict[str, Any]:
|
|
353
|
+
"""Export table as dictionary (useful for JSON)."""
|
|
354
|
+
return {
|
|
355
|
+
"title": self.title,
|
|
356
|
+
"headline": self.headline,
|
|
357
|
+
"columns": [col.to_dict() for col in self.columns],
|
|
358
|
+
"rows": self.active_rows,
|
|
359
|
+
"metadata": {
|
|
360
|
+
"total": self.total_count,
|
|
361
|
+
"returned": self.returned_count,
|
|
362
|
+
"filters_applied": self.filters_applied,
|
|
363
|
+
"sort_by": self.sort_by,
|
|
364
|
+
"selected_columns": self.selected_columns,
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@staticmethod
|
|
369
|
+
def from_dict(data: dict[str, Any]) -> "TableData":
|
|
370
|
+
"""Import table from dictionary."""
|
|
371
|
+
return TableData(
|
|
372
|
+
columns=[ColumnDef.from_dict(col) for col in data.get("columns", [])],
|
|
373
|
+
rows=data.get("rows", []),
|
|
374
|
+
title=data.get("title"),
|
|
375
|
+
headline=data.get("headline") or data.get("description"),
|
|
376
|
+
filters_applied=data.get("metadata", {}).get("filters_applied", {}),
|
|
377
|
+
sort_by=data.get("metadata", {}).get("sort_by", []),
|
|
378
|
+
selected_columns=data.get("metadata", {}).get("selected_columns"),
|
|
379
|
+
total_count=data.get("metadata", {}).get("total"),
|
|
380
|
+
returned_count=data.get("metadata", {}).get("returned"),
|
|
381
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Observability and instrumentation utilities.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- Tracing with OpenTelemetry (otel_init)
|
|
5
|
+
- Span context management (observability)
|
|
6
|
+
- Instrumentation decorators (instrumentation)
|
|
7
|
+
|
|
8
|
+
Use submodule imports for clarity:
|
|
9
|
+
from roadmap.common.observability.instrumentation import traced
|
|
10
|
+
from roadmap.common.observability.observability import get_current_span, set_current_span
|
|
11
|
+
from roadmap.common.observability.otel_init import initialize_tracing, is_tracing_enabled
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .instrumentation import span_context_processor, traced
|
|
15
|
+
from .observability import Span, create_span, get_current_span, set_current_span
|
|
16
|
+
from .otel_init import get_tracer, initialize_tracing, is_tracing_enabled
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"traced",
|
|
20
|
+
"span_context_processor",
|
|
21
|
+
"Span",
|
|
22
|
+
"create_span",
|
|
23
|
+
"get_current_span",
|
|
24
|
+
"set_current_span",
|
|
25
|
+
"get_tracer",
|
|
26
|
+
"initialize_tracing",
|
|
27
|
+
"is_tracing_enabled",
|
|
28
|
+
]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Decorators for automatic span creation and timing.
|
|
2
|
+
|
|
3
|
+
Provides @traced decorator for instrumenting functions with automatic
|
|
4
|
+
trace span creation and duration tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from structlog import get_logger
|
|
12
|
+
|
|
13
|
+
from .observability import create_span, get_current_span, set_current_span
|
|
14
|
+
|
|
15
|
+
logger = get_logger()
|
|
16
|
+
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def traced(operation_name: str) -> Callable[[F], F]:
|
|
21
|
+
"""Create spans for functions automatically.
|
|
22
|
+
|
|
23
|
+
Wraps a function to create a trace span, track duration, and set
|
|
24
|
+
span context in logs. Works with both sync and async functions.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
operation_name: Name of the operation to use in traces
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Decorator function
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
@traced("fetch_items")
|
|
34
|
+
def get_items():
|
|
35
|
+
return [1, 2, 3]
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def decorator(func: F) -> F:
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
span = create_span(operation_name)
|
|
42
|
+
previous_span = get_current_span()
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
set_current_span(span)
|
|
46
|
+
result = func(*args, **kwargs)
|
|
47
|
+
|
|
48
|
+
# Log span completion
|
|
49
|
+
logger.debug(
|
|
50
|
+
"span_completed",
|
|
51
|
+
operation_name=operation_name,
|
|
52
|
+
extra={
|
|
53
|
+
"span_id": span.span_id,
|
|
54
|
+
"parent_span_id": span.parent_span_id,
|
|
55
|
+
"duration_ms": span.duration_ms,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return result
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.error(
|
|
62
|
+
"span_failed",
|
|
63
|
+
operation_name=operation_name,
|
|
64
|
+
extra={
|
|
65
|
+
"span_id": span.span_id,
|
|
66
|
+
"parent_span_id": span.parent_span_id,
|
|
67
|
+
"duration_ms": span.duration_ms,
|
|
68
|
+
"error": str(exc),
|
|
69
|
+
},
|
|
70
|
+
exc_info=True,
|
|
71
|
+
)
|
|
72
|
+
raise
|
|
73
|
+
finally:
|
|
74
|
+
set_current_span(previous_span)
|
|
75
|
+
|
|
76
|
+
return wrapper # type: ignore
|
|
77
|
+
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def span_context_processor(
|
|
82
|
+
logger: Any, name: str, event_dict: dict[str, Any]
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Structlog processor to inject span context into logs.
|
|
85
|
+
|
|
86
|
+
Adds span_id and parent_span_id to all log events from within
|
|
87
|
+
an active span.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
logger: The logger instance
|
|
91
|
+
name: Name of the logging call
|
|
92
|
+
event_dict: The event dictionary
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Modified event dictionary with span context
|
|
96
|
+
"""
|
|
97
|
+
span = get_current_span()
|
|
98
|
+
if span:
|
|
99
|
+
event_dict["span_id"] = span.span_id
|
|
100
|
+
event_dict["parent_span_id"] = span.parent_span_id
|
|
101
|
+
event_dict["span_elapsed_ms"] = span.duration_ms
|
|
102
|
+
|
|
103
|
+
return event_dict
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Minimal trace context management for distributed tracing.
|
|
2
|
+
|
|
3
|
+
Provides simple span tracking and timing for debugging performance
|
|
4
|
+
and correlating logs within a single CLI invocation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import contextvars
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
# Context variable to store current span
|
|
14
|
+
_current_span: contextvars.ContextVar[Optional["Span"]] = contextvars.ContextVar(
|
|
15
|
+
"current_span", default=None
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Span:
|
|
21
|
+
"""Represents a single operation in a trace."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
span_id: str
|
|
25
|
+
start_time: float
|
|
26
|
+
parent_span_id: str | None = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def duration_ms(self) -> float:
|
|
30
|
+
"""Get elapsed time in milliseconds."""
|
|
31
|
+
return (time.time() - self.start_time) * 1000
|
|
32
|
+
|
|
33
|
+
def to_dict(self):
|
|
34
|
+
"""Convert to dict for logging."""
|
|
35
|
+
return {
|
|
36
|
+
"span_id": self.span_id,
|
|
37
|
+
"parent_span_id": self.parent_span_id,
|
|
38
|
+
"span_duration_ms": self.duration_ms,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def create_span(name: str) -> Span:
|
|
43
|
+
"""Create a new span.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Name of the operation being traced
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Span object representing this operation
|
|
50
|
+
"""
|
|
51
|
+
parent = _current_span.get()
|
|
52
|
+
span = Span(
|
|
53
|
+
name=name,
|
|
54
|
+
span_id=uuid.uuid4().hex[:16],
|
|
55
|
+
start_time=time.time(),
|
|
56
|
+
parent_span_id=parent.span_id if parent else None,
|
|
57
|
+
)
|
|
58
|
+
return span
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_current_span(span: Span | None) -> None:
|
|
62
|
+
"""Set the current active span.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
span: Span object to set as current, or None to clear
|
|
66
|
+
"""
|
|
67
|
+
_current_span.set(span)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_current_span() -> Span | None:
|
|
71
|
+
"""Get the current active span.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Current Span object or None if no active span
|
|
75
|
+
"""
|
|
76
|
+
return _current_span.get()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""OpenTelemetry initialization for local tracing.
|
|
2
|
+
|
|
3
|
+
Sets up trace exporter and OTLP integration for local development.
|
|
4
|
+
Exports traces to local OTLP receiver on port 4317.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from structlog import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
10
|
+
|
|
11
|
+
# Global tracer instance
|
|
12
|
+
_tracer: object | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def initialize_tracing(service_name: str = "roadmap-cli") -> None:
|
|
16
|
+
"""Initialize OpenTelemetry tracing with OTLP exporter.
|
|
17
|
+
|
|
18
|
+
This must be called once at application startup before any tracing
|
|
19
|
+
operations. It configures trace export to a local OTLP receiver
|
|
20
|
+
(typically a collector or Jaeger with OTLP native support).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
service_name: Name of the service for tracing (default: "roadmap-cli")
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
from roadmap.common.observability.otel_init import initialize_tracing
|
|
27
|
+
initialize_tracing()
|
|
28
|
+
"""
|
|
29
|
+
global _tracer
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
33
|
+
OTLPSpanExporter,
|
|
34
|
+
)
|
|
35
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
36
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
37
|
+
|
|
38
|
+
# Create OTLP exporter (connects to localhost:4317 by default)
|
|
39
|
+
otlp_exporter = OTLPSpanExporter(
|
|
40
|
+
endpoint="http://localhost:4317",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Create tracer provider
|
|
44
|
+
tracer_provider = TracerProvider()
|
|
45
|
+
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
|
46
|
+
|
|
47
|
+
# Set global tracer provider
|
|
48
|
+
from opentelemetry import trace
|
|
49
|
+
|
|
50
|
+
trace.set_tracer_provider(tracer_provider)
|
|
51
|
+
|
|
52
|
+
_tracer = trace.get_tracer(__name__)
|
|
53
|
+
|
|
54
|
+
logger.debug(
|
|
55
|
+
"opentelemetry_tracing_initialized",
|
|
56
|
+
extra={"service": service_name, "exporter": "otlp"},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
except ImportError as e:
|
|
60
|
+
logger.debug(
|
|
61
|
+
"opentelemetry_not_available",
|
|
62
|
+
error=str(e),
|
|
63
|
+
detail="Tracing features will be disabled. Install with: pip install opentelemetry-exporter-otlp",
|
|
64
|
+
)
|
|
65
|
+
_tracer = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_tracing_enabled() -> bool:
|
|
69
|
+
"""Check if tracing has been initialized.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if tracing is available, False otherwise
|
|
73
|
+
"""
|
|
74
|
+
return _tracer is not None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_tracer():
|
|
78
|
+
"""Get the global tracer instance.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tracer instance if initialized, None otherwise
|
|
82
|
+
"""
|
|
83
|
+
# If tracer is None, return None
|
|
84
|
+
if _tracer is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# If tracer is already callable (e.g., a Mock in tests or a factory),
|
|
88
|
+
# return it directly so existing tests that set Mock() work.
|
|
89
|
+
if callable(_tracer):
|
|
90
|
+
return _tracer
|
|
91
|
+
|
|
92
|
+
# For real Tracer instances (which may not be callable), return a
|
|
93
|
+
# small proxy that is callable and delegates attribute access to the
|
|
94
|
+
# underlying tracer. This keeps `callable(get_tracer())` True while
|
|
95
|
+
# preserving expected tracer behavior.
|
|
96
|
+
class _TracerProxy:
|
|
97
|
+
def __init__(self, tracer):
|
|
98
|
+
self._tracer = tracer
|
|
99
|
+
|
|
100
|
+
def __call__(self, *_args, **_kwargs):
|
|
101
|
+
return self._tracer
|
|
102
|
+
|
|
103
|
+
def __getattr__(self, name):
|
|
104
|
+
return getattr(self._tracer, name)
|
|
105
|
+
|
|
106
|
+
def __repr__(self):
|
|
107
|
+
return f"<TracerProxy for {self._tracer!r}>"
|
|
108
|
+
|
|
109
|
+
return _TracerProxy(_tracer)
|