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,161 @@
|
|
|
1
|
+
"""Metrics collection and reporting for monitoring operation performance."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..logging import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class OperationMetric:
|
|
14
|
+
"""Track operation performance and success.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
operation: Name of the operation being tracked
|
|
18
|
+
duration_ms: Duration of the operation in milliseconds
|
|
19
|
+
success: Whether the operation succeeded
|
|
20
|
+
error: Error message if operation failed, None otherwise
|
|
21
|
+
timestamp: When the operation completed (UTC)
|
|
22
|
+
metadata: Additional context about the operation
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
operation: str
|
|
26
|
+
duration_ms: float
|
|
27
|
+
success: bool
|
|
28
|
+
error: str | None = None
|
|
29
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
30
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MetricsCollector:
|
|
34
|
+
"""Collect and report application metrics.
|
|
35
|
+
|
|
36
|
+
This class maintains a collection of operation metrics and provides
|
|
37
|
+
aggregated statistics for monitoring and observability.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
"""Initialize an empty metrics collector."""
|
|
42
|
+
self.metrics: list[OperationMetric] = []
|
|
43
|
+
logger.debug("metrics_collector_initialized")
|
|
44
|
+
|
|
45
|
+
def record(self, metric: OperationMetric) -> None:
|
|
46
|
+
"""Record a metric.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
metric: The operation metric to record
|
|
50
|
+
"""
|
|
51
|
+
self.metrics.append(metric)
|
|
52
|
+
logger.debug(
|
|
53
|
+
"metric_recorded",
|
|
54
|
+
operation=metric.operation,
|
|
55
|
+
duration_ms=metric.duration_ms,
|
|
56
|
+
success=metric.success,
|
|
57
|
+
error=metric.error,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def get_stats(self) -> dict[str, Any]:
|
|
61
|
+
"""Get aggregated statistics from recorded metrics.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dictionary containing:
|
|
65
|
+
- total_operations: Total number of operations recorded
|
|
66
|
+
- success_rate: Percentage of successful operations (0.0-1.0)
|
|
67
|
+
- avg_duration_ms: Average operation duration in milliseconds
|
|
68
|
+
- errors: List of failed operation metrics
|
|
69
|
+
- operations_by_type: Count of operations grouped by type
|
|
70
|
+
"""
|
|
71
|
+
if not self.metrics:
|
|
72
|
+
return {
|
|
73
|
+
"total_operations": 0,
|
|
74
|
+
"success_rate": 0.0,
|
|
75
|
+
"avg_duration_ms": 0.0,
|
|
76
|
+
"errors": [],
|
|
77
|
+
"operations_by_type": {},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
total = len(self.metrics)
|
|
81
|
+
successful = sum(1 for m in self.metrics if m.success)
|
|
82
|
+
success_rate = successful / total if total > 0 else 0.0
|
|
83
|
+
avg_duration = sum(m.duration_ms for m in self.metrics) / total
|
|
84
|
+
errors = [m for m in self.metrics if not m.success]
|
|
85
|
+
|
|
86
|
+
# Count operations by type
|
|
87
|
+
operations_by_type: dict[str, int] = {}
|
|
88
|
+
for metric in self.metrics:
|
|
89
|
+
operations_by_type[metric.operation] = (
|
|
90
|
+
operations_by_type.get(metric.operation, 0) + 1
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
"total_operations": total,
|
|
95
|
+
"success_rate": success_rate,
|
|
96
|
+
"avg_duration_ms": avg_duration,
|
|
97
|
+
"errors": errors,
|
|
98
|
+
"operations_by_type": operations_by_type,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def clear(self) -> None:
|
|
102
|
+
"""Clear all recorded metrics."""
|
|
103
|
+
count = len(self.metrics)
|
|
104
|
+
self.metrics.clear()
|
|
105
|
+
logger.debug("metrics_cleared", count=count)
|
|
106
|
+
|
|
107
|
+
def get_error_rate(self) -> float:
|
|
108
|
+
"""Get the error rate (1 - success_rate).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Error rate as a float between 0.0 and 1.0
|
|
112
|
+
"""
|
|
113
|
+
if not self.metrics:
|
|
114
|
+
return 0.0
|
|
115
|
+
return 1.0 - self.get_stats()["success_rate"]
|
|
116
|
+
|
|
117
|
+
def get_operation_stats(self, operation: str) -> dict[str, Any]:
|
|
118
|
+
"""Get statistics for a specific operation type.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
operation: Name of the operation to get stats for
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary with stats specific to this operation type
|
|
125
|
+
"""
|
|
126
|
+
op_metrics = [m for m in self.metrics if m.operation == operation]
|
|
127
|
+
|
|
128
|
+
if not op_metrics:
|
|
129
|
+
return {
|
|
130
|
+
"count": 0,
|
|
131
|
+
"success_rate": 0.0,
|
|
132
|
+
"avg_duration_ms": 0.0,
|
|
133
|
+
"errors": [],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
total = len(op_metrics)
|
|
137
|
+
successful = sum(1 for m in op_metrics if m.success)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
"count": total,
|
|
141
|
+
"success_rate": successful / total,
|
|
142
|
+
"avg_duration_ms": sum(m.duration_ms for m in op_metrics) / total,
|
|
143
|
+
"errors": [m for m in op_metrics if not m.success],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Global metrics collector instance
|
|
148
|
+
_global_collector: MetricsCollector | None = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_metrics_collector() -> MetricsCollector:
|
|
152
|
+
"""Get or create the global metrics collector instance.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The global MetricsCollector instance
|
|
156
|
+
"""
|
|
157
|
+
global _global_collector
|
|
158
|
+
if _global_collector is None:
|
|
159
|
+
_global_collector = MetricsCollector()
|
|
160
|
+
logger.debug("global_metrics_collector_created")
|
|
161
|
+
return _global_collector
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Performance monitoring decorators and utilities."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
from ..logging import get_logger
|
|
9
|
+
from .metrics import OperationMetric, get_metrics_collector
|
|
10
|
+
from .profiling import get_profiler
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def timed_operation(operation_name: str | None = None, record_metric: bool = True):
|
|
18
|
+
"""Time operations and optionally record metrics.
|
|
19
|
+
|
|
20
|
+
This decorator measures the execution time of a function and logs it.
|
|
21
|
+
It can also record the timing as a metric in the global MetricsCollector
|
|
22
|
+
and performance profiler.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
operation_name: Name for the operation (defaults to function name)
|
|
26
|
+
record_metric: Whether to record the timing as a metric
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
@timed_operation("create_issue")
|
|
30
|
+
def create_issue(title: str) -> Issue:
|
|
31
|
+
# ... implementation
|
|
32
|
+
pass
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def decorator(func: F) -> F:
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
38
|
+
op_name = operation_name or func.__name__
|
|
39
|
+
start_time = time.perf_counter()
|
|
40
|
+
error: Exception | None = None
|
|
41
|
+
result: Any = None
|
|
42
|
+
|
|
43
|
+
# Record in profiler
|
|
44
|
+
profiler = get_profiler()
|
|
45
|
+
profiler.start_operation(op_name)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
result = func(*args, **kwargs)
|
|
49
|
+
return result
|
|
50
|
+
except Exception as e:
|
|
51
|
+
error = e
|
|
52
|
+
raise
|
|
53
|
+
finally:
|
|
54
|
+
# Calculate duration
|
|
55
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
56
|
+
|
|
57
|
+
# Record in profiler
|
|
58
|
+
profiler.end_operation(op_name, error=error is not None)
|
|
59
|
+
|
|
60
|
+
# Log the timing
|
|
61
|
+
if error:
|
|
62
|
+
logger.error(
|
|
63
|
+
"operation_failed",
|
|
64
|
+
operation=op_name,
|
|
65
|
+
duration_ms=round(duration_ms, 2),
|
|
66
|
+
error=str(error),
|
|
67
|
+
error_type=type(error).__name__,
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
logger.info(
|
|
71
|
+
"operation_completed",
|
|
72
|
+
operation=op_name,
|
|
73
|
+
duration_ms=round(duration_ms, 2),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Record metric if requested
|
|
77
|
+
if record_metric:
|
|
78
|
+
collector = get_metrics_collector()
|
|
79
|
+
metric = OperationMetric(
|
|
80
|
+
operation=op_name,
|
|
81
|
+
duration_ms=duration_ms,
|
|
82
|
+
success=error is None,
|
|
83
|
+
error=str(error) if error else None,
|
|
84
|
+
)
|
|
85
|
+
collector.record(metric)
|
|
86
|
+
|
|
87
|
+
return wrapper # type: ignore
|
|
88
|
+
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def async_timed_operation(
|
|
93
|
+
operation_name: str | None = None, record_metric: bool = True
|
|
94
|
+
):
|
|
95
|
+
"""Async version of timed_operation decorator.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
operation_name: Name for the operation (defaults to function name)
|
|
99
|
+
record_metric: Whether to record the timing as a metric
|
|
100
|
+
|
|
101
|
+
Example:
|
|
102
|
+
@async_timed_operation("fetch_issues")
|
|
103
|
+
async def fetch_issues() -> list[Issue]:
|
|
104
|
+
# ... async implementation
|
|
105
|
+
pass
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def decorator(func: F) -> F:
|
|
109
|
+
@functools.wraps(func)
|
|
110
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
111
|
+
op_name = operation_name or func.__name__
|
|
112
|
+
start_time = time.perf_counter()
|
|
113
|
+
error: Exception | None = None
|
|
114
|
+
result: Any = None
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
result = await func(*args, **kwargs)
|
|
118
|
+
return result
|
|
119
|
+
except Exception as e:
|
|
120
|
+
error = e
|
|
121
|
+
raise
|
|
122
|
+
finally:
|
|
123
|
+
# Calculate duration
|
|
124
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
125
|
+
|
|
126
|
+
# Log the timing
|
|
127
|
+
if error:
|
|
128
|
+
logger.error(
|
|
129
|
+
"async_operation_failed",
|
|
130
|
+
operation=op_name,
|
|
131
|
+
duration_ms=round(duration_ms, 2),
|
|
132
|
+
error=str(error),
|
|
133
|
+
error_type=type(error).__name__,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
logger.info(
|
|
137
|
+
"async_operation_completed",
|
|
138
|
+
operation=op_name,
|
|
139
|
+
duration_ms=round(duration_ms, 2),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Record metric if requested
|
|
143
|
+
if record_metric:
|
|
144
|
+
collector = get_metrics_collector()
|
|
145
|
+
metric = OperationMetric(
|
|
146
|
+
operation=op_name,
|
|
147
|
+
duration_ms=duration_ms,
|
|
148
|
+
success=error is None,
|
|
149
|
+
error=str(error) if error else None,
|
|
150
|
+
)
|
|
151
|
+
collector.record(metric)
|
|
152
|
+
|
|
153
|
+
return wrapper # type: ignore
|
|
154
|
+
|
|
155
|
+
return decorator
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class OperationTimer:
|
|
159
|
+
"""Context manager for timing code blocks.
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
with OperationTimer("database_query") as timer:
|
|
163
|
+
# ... perform operation
|
|
164
|
+
pass
|
|
165
|
+
print(f"Operation took {timer.duration_ms}ms")
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, operation_name: str, record_metric: bool = True):
|
|
169
|
+
"""Initialize the timer.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
operation_name: Name of the operation being timed
|
|
173
|
+
record_metric: Whether to record the timing as a metric
|
|
174
|
+
"""
|
|
175
|
+
self.operation_name = operation_name
|
|
176
|
+
self.record_metric = record_metric
|
|
177
|
+
self.start_time: float | None = None
|
|
178
|
+
self.end_time: float | None = None
|
|
179
|
+
self.duration_ms: float | None = None
|
|
180
|
+
self.error: Exception | None = None
|
|
181
|
+
|
|
182
|
+
def __enter__(self) -> "OperationTimer":
|
|
183
|
+
"""Start the timer."""
|
|
184
|
+
self.start_time = time.perf_counter()
|
|
185
|
+
logger.debug("operation_started", operation=self.operation_name)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def __exit__(self, _exc_type, exc_val, _exc_tb) -> None:
|
|
189
|
+
"""Stop the timer and record metrics."""
|
|
190
|
+
self.end_time = time.perf_counter()
|
|
191
|
+
assert self.start_time is not None
|
|
192
|
+
self.duration_ms = (self.end_time - self.start_time) * 1000
|
|
193
|
+
|
|
194
|
+
if exc_val:
|
|
195
|
+
self.error = exc_val
|
|
196
|
+
logger.error(
|
|
197
|
+
"operation_failed",
|
|
198
|
+
operation=self.operation_name,
|
|
199
|
+
duration_ms=round(self.duration_ms, 2),
|
|
200
|
+
error=str(exc_val),
|
|
201
|
+
error_type=type(exc_val).__name__,
|
|
202
|
+
)
|
|
203
|
+
else:
|
|
204
|
+
logger.info(
|
|
205
|
+
"operation_completed",
|
|
206
|
+
operation=self.operation_name,
|
|
207
|
+
duration_ms=round(self.duration_ms, 2),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Record metric if requested
|
|
211
|
+
if self.record_metric:
|
|
212
|
+
collector = get_metrics_collector()
|
|
213
|
+
metric = OperationMetric(
|
|
214
|
+
operation=self.operation_name,
|
|
215
|
+
duration_ms=self.duration_ms,
|
|
216
|
+
success=exc_val is None,
|
|
217
|
+
error=str(exc_val) if exc_val else None,
|
|
218
|
+
)
|
|
219
|
+
collector.record(metric)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Performance benchmarking and profiling utilities.
|
|
2
|
+
|
|
3
|
+
This module provides tools for measuring and analyzing performance of CLI operations,
|
|
4
|
+
including execution timing, memory usage, and operation throughput.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class OperationProfile:
|
|
18
|
+
"""Profile data for a single operation type.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
operation: Name of the operation
|
|
22
|
+
count: Number of times operation was executed
|
|
23
|
+
total_time_ms: Total execution time in milliseconds
|
|
24
|
+
min_time_ms: Minimum single execution time
|
|
25
|
+
max_time_ms: Maximum single execution time
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
operation: str
|
|
29
|
+
count: int = 0
|
|
30
|
+
total_time_ms: float = 0.0
|
|
31
|
+
min_time_ms: float = float("inf")
|
|
32
|
+
max_time_ms: float = 0.0
|
|
33
|
+
errors: int = 0
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def avg_time_ms(self) -> float:
|
|
37
|
+
"""Calculate average execution time."""
|
|
38
|
+
return self.total_time_ms / self.count if self.count > 0 else 0.0
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def throughput_per_sec(self) -> float:
|
|
42
|
+
"""Calculate operations per second."""
|
|
43
|
+
if self.total_time_ms <= 0:
|
|
44
|
+
return 0.0
|
|
45
|
+
return (self.count / self.total_time_ms) * 1000
|
|
46
|
+
|
|
47
|
+
def record(self, duration_ms: float, error: bool = False) -> None:
|
|
48
|
+
"""Record a single operation execution.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
duration_ms: Execution time in milliseconds
|
|
52
|
+
error: Whether the operation failed
|
|
53
|
+
"""
|
|
54
|
+
self.count += 1
|
|
55
|
+
self.total_time_ms += duration_ms
|
|
56
|
+
self.min_time_ms = min(self.min_time_ms, duration_ms)
|
|
57
|
+
self.max_time_ms = max(self.max_time_ms, duration_ms)
|
|
58
|
+
if error:
|
|
59
|
+
self.errors += 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PerformanceProfiler:
|
|
63
|
+
"""Profiles operation performance metrics across a session.
|
|
64
|
+
|
|
65
|
+
Tracks execution time, error rates, and throughput for all operations,
|
|
66
|
+
providing detailed performance insights.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
profiler = PerformanceProfiler()
|
|
70
|
+
profiler.start_operation("fetch_issues")
|
|
71
|
+
# ... do work
|
|
72
|
+
profiler.end_operation("fetch_issues")
|
|
73
|
+
|
|
74
|
+
report = profiler.get_report()
|
|
75
|
+
print(report.format())
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
"""Initialize profiler with no operations."""
|
|
80
|
+
self._profiles: dict[str, OperationProfile] = {}
|
|
81
|
+
self._start_times: dict[str, float] = {}
|
|
82
|
+
logger.debug("profiler_initialized")
|
|
83
|
+
|
|
84
|
+
def start_operation(self, operation: str) -> None:
|
|
85
|
+
"""Mark the start of an operation.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
operation: Name of the operation
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
profiler.start_operation("sync_all")
|
|
92
|
+
"""
|
|
93
|
+
self._start_times[operation] = time.perf_counter()
|
|
94
|
+
|
|
95
|
+
def end_operation(self, operation: str, error: bool = False) -> float:
|
|
96
|
+
"""Mark the end of an operation and record metrics.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
operation: Name of the operation
|
|
100
|
+
error: Whether the operation failed
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Duration in milliseconds
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
try:
|
|
107
|
+
# ... do work
|
|
108
|
+
profiler.end_operation("sync_all")
|
|
109
|
+
except Exception:
|
|
110
|
+
profiler.end_operation("sync_all", error=True)
|
|
111
|
+
raise
|
|
112
|
+
"""
|
|
113
|
+
if operation not in self._start_times:
|
|
114
|
+
logger.warning("operation_not_started", operation=operation)
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
start = self._start_times.pop(operation)
|
|
118
|
+
duration_ms = (time.perf_counter() - start) * 1000
|
|
119
|
+
|
|
120
|
+
if operation not in self._profiles:
|
|
121
|
+
self._profiles[operation] = OperationProfile(operation)
|
|
122
|
+
|
|
123
|
+
self._profiles[operation].record(duration_ms, error=error)
|
|
124
|
+
logger.debug(
|
|
125
|
+
"operation_profiled",
|
|
126
|
+
operation=operation,
|
|
127
|
+
duration_ms=duration_ms,
|
|
128
|
+
error=error,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return duration_ms
|
|
132
|
+
|
|
133
|
+
def get_profile(self, operation: str) -> OperationProfile | None:
|
|
134
|
+
"""Get profile for a specific operation.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
operation: Name of the operation
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
OperationProfile if found, None otherwise
|
|
141
|
+
"""
|
|
142
|
+
return self._profiles.get(operation)
|
|
143
|
+
|
|
144
|
+
def get_slowest_operations(self, limit: int = 10) -> list[OperationProfile]:
|
|
145
|
+
"""Get the slowest operations by average execution time.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
limit: Maximum number of results to return
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
List of operations sorted by average time (slowest first)
|
|
152
|
+
"""
|
|
153
|
+
sorted_ops = sorted(
|
|
154
|
+
self._profiles.values(), key=lambda p: p.avg_time_ms, reverse=True
|
|
155
|
+
)
|
|
156
|
+
return sorted_ops[:limit]
|
|
157
|
+
|
|
158
|
+
def get_report(self) -> "PerformanceReport":
|
|
159
|
+
"""Generate a performance report for all operations.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
PerformanceReport with aggregated metrics
|
|
163
|
+
"""
|
|
164
|
+
total_time_ms = sum(p.total_time_ms for p in self._profiles.values())
|
|
165
|
+
total_count = sum(p.count for p in self._profiles.values())
|
|
166
|
+
total_errors = sum(p.errors for p in self._profiles.values())
|
|
167
|
+
|
|
168
|
+
return PerformanceReport(
|
|
169
|
+
profiles=self._profiles.copy(),
|
|
170
|
+
total_time_ms=total_time_ms,
|
|
171
|
+
total_operations=total_count,
|
|
172
|
+
total_errors=total_errors,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def clear(self) -> None:
|
|
176
|
+
"""Clear all profiles."""
|
|
177
|
+
self._profiles.clear()
|
|
178
|
+
self._start_times.clear()
|
|
179
|
+
logger.debug("profiler_cleared")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class PerformanceReport:
|
|
184
|
+
"""Aggregated performance metrics for a session.
|
|
185
|
+
|
|
186
|
+
Attributes:
|
|
187
|
+
profiles: Dictionary of operation profiles
|
|
188
|
+
total_time_ms: Total execution time
|
|
189
|
+
total_operations: Total operations executed
|
|
190
|
+
total_errors: Total failed operations
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
profiles: dict[str, OperationProfile]
|
|
194
|
+
total_time_ms: float
|
|
195
|
+
total_operations: int
|
|
196
|
+
total_errors: int
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def success_rate(self) -> float:
|
|
200
|
+
"""Calculate overall success rate (0.0-1.0)."""
|
|
201
|
+
if self.total_operations == 0:
|
|
202
|
+
return 0.0
|
|
203
|
+
return (self.total_operations - self.total_errors) / self.total_operations
|
|
204
|
+
|
|
205
|
+
def format(self) -> str:
|
|
206
|
+
"""Format report as human-readable string.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Formatted performance report
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
report = profiler.get_report()
|
|
213
|
+
print(report.format())
|
|
214
|
+
"""
|
|
215
|
+
lines = [
|
|
216
|
+
"═" * 70,
|
|
217
|
+
"PERFORMANCE REPORT",
|
|
218
|
+
"═" * 70,
|
|
219
|
+
f"Total Time: {self.total_time_ms:.2f}ms",
|
|
220
|
+
f"Total Operations: {self.total_operations}",
|
|
221
|
+
f"Success Rate: {self.success_rate:.1%}",
|
|
222
|
+
"",
|
|
223
|
+
"Operation Breakdown:",
|
|
224
|
+
"-" * 70,
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
# Sort by total time
|
|
228
|
+
sorted_ops = sorted(
|
|
229
|
+
self.profiles.values(), key=lambda p: p.total_time_ms, reverse=True
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
for profile in sorted_ops:
|
|
233
|
+
pct = (
|
|
234
|
+
(profile.total_time_ms / self.total_time_ms * 100)
|
|
235
|
+
if self.total_time_ms > 0
|
|
236
|
+
else 0
|
|
237
|
+
)
|
|
238
|
+
error_str = f" ({profile.errors} errors)" if profile.errors > 0 else ""
|
|
239
|
+
|
|
240
|
+
lines.append(
|
|
241
|
+
f"{profile.operation:30} {profile.count:5} ops "
|
|
242
|
+
f"{profile.avg_time_ms:8.2f}ms avg {pct:5.1f}%{error_str}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
lines.append("═" * 70)
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
def get_dict(self) -> dict[str, Any]:
|
|
249
|
+
"""Convert report to dictionary format.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dictionary representation of the report
|
|
253
|
+
"""
|
|
254
|
+
return {
|
|
255
|
+
"total_time_ms": self.total_time_ms,
|
|
256
|
+
"total_operations": self.total_operations,
|
|
257
|
+
"success_rate": self.success_rate,
|
|
258
|
+
"total_errors": self.total_errors,
|
|
259
|
+
"operations": {
|
|
260
|
+
name: {
|
|
261
|
+
"count": profile.count,
|
|
262
|
+
"total_ms": profile.total_time_ms,
|
|
263
|
+
"avg_ms": profile.avg_time_ms,
|
|
264
|
+
"min_ms": profile.min_time_ms if profile.count > 0 else 0,
|
|
265
|
+
"max_ms": profile.max_time_ms,
|
|
266
|
+
"throughput_ops_per_sec": profile.throughput_per_sec,
|
|
267
|
+
"errors": profile.errors,
|
|
268
|
+
}
|
|
269
|
+
for name, profile in self.profiles.items()
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# Global profiler instance
|
|
275
|
+
_profiler = PerformanceProfiler()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_profiler() -> PerformanceProfiler:
|
|
279
|
+
"""Get the global profiler instance.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The singleton PerformanceProfiler instance
|
|
283
|
+
|
|
284
|
+
Example:
|
|
285
|
+
profiler = get_profiler()
|
|
286
|
+
profiler.start_operation("my_op")
|
|
287
|
+
"""
|
|
288
|
+
return _profiler
|