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
roadmap/common/result.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Result type for functional error handling.
|
|
2
|
+
|
|
3
|
+
This module provides a Result[T, E] type for railway-oriented programming,
|
|
4
|
+
enabling explicit error handling without exceptions. Based on Rust's Result type.
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
>>> def divide(a: int, b: int) -> Result[float, str]:
|
|
8
|
+
... if b == 0:
|
|
9
|
+
... return Err("Division by zero")
|
|
10
|
+
... return Ok(a / b)
|
|
11
|
+
...
|
|
12
|
+
>>> result = divide(10, 2)
|
|
13
|
+
>>> if result.is_ok():
|
|
14
|
+
... print(f"Success: {result.unwrap()}")
|
|
15
|
+
...
|
|
16
|
+
>>> result = divide(10, 0)
|
|
17
|
+
>>> if result.is_err():
|
|
18
|
+
... print(f"Error: {result.unwrap_err()}")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Any, Generic, NoReturn, TypeVar, Union
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T") # Success type
|
|
28
|
+
E = TypeVar("E") # Error type
|
|
29
|
+
U = TypeVar("U") # Mapped success type
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Ok(Generic[T]):
|
|
34
|
+
"""Success variant of Result type.
|
|
35
|
+
|
|
36
|
+
Contains a successful value of type T.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
value: The successful result value
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
value: T
|
|
43
|
+
|
|
44
|
+
def is_ok(self) -> bool:
|
|
45
|
+
"""Check if this is an Ok variant.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Always True for Ok
|
|
49
|
+
"""
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def is_err(self) -> bool:
|
|
53
|
+
"""Check if this is an Err variant.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Always False for Ok
|
|
57
|
+
"""
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def unwrap(self) -> T:
|
|
61
|
+
"""Extract the success value.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The contained value
|
|
65
|
+
"""
|
|
66
|
+
return self.value
|
|
67
|
+
|
|
68
|
+
def unwrap_err(self) -> NoReturn:
|
|
69
|
+
"""Attempt to extract error (raises exception).
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ValueError: Always, since Ok doesn't contain an error
|
|
73
|
+
"""
|
|
74
|
+
raise ValueError(f"Called unwrap_err() on Ok value: {self.value}")
|
|
75
|
+
|
|
76
|
+
def unwrap_or(self, default: T) -> T:
|
|
77
|
+
"""Extract value or return default.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
default: Value to return if this is Err (unused)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The contained value
|
|
84
|
+
"""
|
|
85
|
+
return self.value
|
|
86
|
+
|
|
87
|
+
def unwrap_or_else(self, op: Callable[[E], T]) -> T:
|
|
88
|
+
"""Extract value or compute from error.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
op: Function to compute default from error (unused)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The contained value
|
|
95
|
+
"""
|
|
96
|
+
return self.value
|
|
97
|
+
|
|
98
|
+
def map(self, op: Callable[[T], U]) -> Result[U, E]:
|
|
99
|
+
"""Map the success value through a function.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
op: Function to transform the value
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Ok with transformed value
|
|
106
|
+
"""
|
|
107
|
+
return Ok(op(self.value))
|
|
108
|
+
|
|
109
|
+
def map_err(self, op: Callable[[E], Any]) -> Result[T, Any]:
|
|
110
|
+
"""Map the error through a function (no-op for Ok).
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
op: Function to transform error (unused)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Self unchanged
|
|
117
|
+
"""
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def and_then(self, op: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
121
|
+
"""Chain another Result-returning operation.
|
|
122
|
+
|
|
123
|
+
Also known as flatMap or bind. Enables railway-oriented programming.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
op: Function that takes success value and returns new Result
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Result from applying op to contained value
|
|
130
|
+
"""
|
|
131
|
+
return op(self.value)
|
|
132
|
+
|
|
133
|
+
def or_else(self, op: Callable[[E], Result[T, Any]]) -> Result[T, Any]:
|
|
134
|
+
"""Provide alternative on error (no-op for Ok).
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
op: Function to compute alternative Result (unused)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Self unchanged
|
|
141
|
+
"""
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class Err(Generic[E]):
|
|
147
|
+
"""Error variant of Result type.
|
|
148
|
+
|
|
149
|
+
Contains an error value of type E.
|
|
150
|
+
|
|
151
|
+
Attributes:
|
|
152
|
+
error: The error value
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
error: E
|
|
156
|
+
|
|
157
|
+
def is_ok(self) -> bool:
|
|
158
|
+
"""Check if this is an Ok variant.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Always False for Err
|
|
162
|
+
"""
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
def is_err(self) -> bool:
|
|
166
|
+
"""Check if this is an Err variant.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Always True for Err
|
|
170
|
+
"""
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
def unwrap(self) -> NoReturn:
|
|
174
|
+
"""Attempt to extract value (raises exception).
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ValueError: Always, since Err doesn't contain a value
|
|
178
|
+
"""
|
|
179
|
+
raise ValueError(f"Called unwrap() on Err value: {self.error}")
|
|
180
|
+
|
|
181
|
+
def unwrap_err(self) -> E:
|
|
182
|
+
"""Extract the error value.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The contained error
|
|
186
|
+
"""
|
|
187
|
+
return self.error
|
|
188
|
+
|
|
189
|
+
def unwrap_or(self, default: T) -> T:
|
|
190
|
+
"""Extract value or return default.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
default: Value to return since this is Err
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The default value
|
|
197
|
+
"""
|
|
198
|
+
return default
|
|
199
|
+
|
|
200
|
+
def unwrap_or_else(self, op: Callable[[E], T]) -> T:
|
|
201
|
+
"""Extract value or compute from error.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
op: Function to compute default from error
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Result of applying op to the error
|
|
208
|
+
"""
|
|
209
|
+
return op(self.error)
|
|
210
|
+
|
|
211
|
+
def map(self, op: Callable[[T], U]) -> Result[U, E]:
|
|
212
|
+
"""Map the success value through a function (no-op for Err).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
op: Function to transform value (unused)
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Self unchanged (with correct type)
|
|
219
|
+
"""
|
|
220
|
+
return Err(self.error)
|
|
221
|
+
|
|
222
|
+
def map_err(self, op: Callable[[E], Any]) -> Result[T, Any]:
|
|
223
|
+
"""Map the error through a function.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
op: Function to transform the error
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Err with transformed error
|
|
230
|
+
"""
|
|
231
|
+
return Err(op(self.error))
|
|
232
|
+
|
|
233
|
+
def and_then(self, op: Callable[[T], Result[U, E]]) -> Result[U, E]:
|
|
234
|
+
"""Chain another Result-returning operation (no-op for Err).
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
op: Function to chain (unused)
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Self unchanged (with correct type)
|
|
241
|
+
"""
|
|
242
|
+
return Err(self.error)
|
|
243
|
+
|
|
244
|
+
def or_else(self, op: Callable[[E], Result[T, Any]]) -> Result[T, Any]:
|
|
245
|
+
"""Provide alternative on error.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
op: Function to compute alternative Result from error
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Result from applying op to error
|
|
252
|
+
"""
|
|
253
|
+
return op(self.error)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# Type alias for Result[T, E]
|
|
257
|
+
Result = Union[Ok[T], Err[E]]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# Helper functions for working with Results
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def wrap_result(func: Callable[..., T]) -> Callable[..., Result[T, Exception]]:
|
|
264
|
+
"""Decorator to convert exception-raising functions to Result-returning.
|
|
265
|
+
|
|
266
|
+
Catches any exception and returns it as Err, or Ok on success.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
func: Function that may raise exceptions
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Wrapped function that returns Result[T, Exception]
|
|
273
|
+
|
|
274
|
+
Example:
|
|
275
|
+
>>> @wrap_result
|
|
276
|
+
... def may_fail(x: int) -> int:
|
|
277
|
+
... if x < 0:
|
|
278
|
+
... raise ValueError("Negative")
|
|
279
|
+
... return x * 2
|
|
280
|
+
...
|
|
281
|
+
>>> result = may_fail(5) # Ok(10)
|
|
282
|
+
>>> result = may_fail(-1) # Err(ValueError("Negative"))
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def wrapper(*args: Any, **kwargs: Any) -> Result[T, Exception]:
|
|
286
|
+
try:
|
|
287
|
+
return Ok(func(*args, **kwargs))
|
|
288
|
+
except Exception as e:
|
|
289
|
+
return Err(e)
|
|
290
|
+
|
|
291
|
+
return wrapper
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def collect_results(results: list[Result[T, E]]) -> Result[list[T], E]:
|
|
295
|
+
"""Collect a list of Results into a Result of list.
|
|
296
|
+
|
|
297
|
+
Returns Ok with list of all success values if all Results are Ok.
|
|
298
|
+
Returns the first Err if any Result is Err.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
results: List of Result values
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Ok with list of values, or first Err encountered
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> results = [Ok(1), Ok(2), Ok(3)]
|
|
308
|
+
>>> collect_results(results) # Ok([1, 2, 3])
|
|
309
|
+
>>>
|
|
310
|
+
>>> results = [Ok(1), Err("fail"), Ok(3)]
|
|
311
|
+
>>> collect_results(results) # Err("fail")
|
|
312
|
+
"""
|
|
313
|
+
values: list[T] = []
|
|
314
|
+
for result in results:
|
|
315
|
+
if result.is_err():
|
|
316
|
+
return result # type: ignore[return-value]
|
|
317
|
+
values.append(result.unwrap())
|
|
318
|
+
return Ok(values)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def partition_results(results: list[Result[T, E]]) -> tuple[list[T], list[E]]:
|
|
322
|
+
"""Partition a list of Results into successes and errors.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
results: List of Result values
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple of (success_values, error_values)
|
|
329
|
+
|
|
330
|
+
Example:
|
|
331
|
+
>>> results = [Ok(1), Err("a"), Ok(2), Err("b")]
|
|
332
|
+
>>> successes, errors = partition_results(results)
|
|
333
|
+
>>> successes # [1, 2]
|
|
334
|
+
>>> errors # ["a", "b"]
|
|
335
|
+
"""
|
|
336
|
+
successes: list[T] = []
|
|
337
|
+
errors: list[E] = []
|
|
338
|
+
for result in results:
|
|
339
|
+
if result.is_ok():
|
|
340
|
+
successes.append(result.unwrap())
|
|
341
|
+
else:
|
|
342
|
+
errors.append(result.unwrap_err())
|
|
343
|
+
return successes, errors
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def all_ok(results: list[Result[T, E]]) -> bool:
|
|
347
|
+
"""Check if all Results in a list are Ok.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
results: List of Result values
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if all are Ok, False if any are Err
|
|
354
|
+
"""
|
|
355
|
+
return all(r.is_ok() for r in results)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def any_err(results: list[Result[T, E]]) -> bool:
|
|
359
|
+
"""Check if any Results in a list are Err.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
results: List of Result values
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
True if any are Err, False if all are Ok
|
|
366
|
+
"""
|
|
367
|
+
return any(r.is_err() for r in results)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def first_err(results: list[Result[T, E]]) -> Result[None, E]:
|
|
371
|
+
"""Get the first error from a list of Results.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
results: List of Result values
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
First Err found, or Ok(None) if all are Ok
|
|
378
|
+
"""
|
|
379
|
+
for result in results:
|
|
380
|
+
if result.is_err():
|
|
381
|
+
return Err(result.unwrap_err())
|
|
382
|
+
return Ok(None)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Export public API
|
|
386
|
+
__all__ = [
|
|
387
|
+
"Ok",
|
|
388
|
+
"Err",
|
|
389
|
+
"Result",
|
|
390
|
+
"wrap_result",
|
|
391
|
+
"collect_results",
|
|
392
|
+
"partition_results",
|
|
393
|
+
"all_ok",
|
|
394
|
+
"any_err",
|
|
395
|
+
"first_err",
|
|
396
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Security utilities for roadmap CLI application.
|
|
2
|
+
|
|
3
|
+
This package provides security functionality organized by concern:
|
|
4
|
+
- exceptions: Security exception types
|
|
5
|
+
- logging: Security event logging
|
|
6
|
+
- path_validation: Path validation and safety checks
|
|
7
|
+
- file_operations: Secure file and directory operations
|
|
8
|
+
- filename_sanitization: Filename sanitization
|
|
9
|
+
- temp_files: Secure temporary file handling
|
|
10
|
+
- export_cleanup: Export validation and backup cleanup
|
|
11
|
+
|
|
12
|
+
For backward compatibility, all public classes and functions are re-exported
|
|
13
|
+
from the root module.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .exceptions import PathValidationError, SecurityError
|
|
17
|
+
from .export_cleanup import cleanup_old_backups, validate_export_size
|
|
18
|
+
from .file_operations import (
|
|
19
|
+
create_secure_directory,
|
|
20
|
+
create_secure_file,
|
|
21
|
+
secure_file_permissions,
|
|
22
|
+
)
|
|
23
|
+
from .filename_sanitization import sanitize_filename
|
|
24
|
+
from .logging import log_security_event, security_logger
|
|
25
|
+
from .path_validation import validate_path
|
|
26
|
+
from .temp_files import create_secure_temp_file
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Exceptions
|
|
30
|
+
"SecurityError",
|
|
31
|
+
"PathValidationError",
|
|
32
|
+
# Logging
|
|
33
|
+
"log_security_event",
|
|
34
|
+
"security_logger",
|
|
35
|
+
# Path validation
|
|
36
|
+
"validate_path",
|
|
37
|
+
# File operations
|
|
38
|
+
"create_secure_file",
|
|
39
|
+
"create_secure_directory",
|
|
40
|
+
"secure_file_permissions",
|
|
41
|
+
# Filename sanitization
|
|
42
|
+
"sanitize_filename",
|
|
43
|
+
# Temp files
|
|
44
|
+
"create_secure_temp_file",
|
|
45
|
+
# Export/backup
|
|
46
|
+
"validate_export_size",
|
|
47
|
+
"cleanup_old_backups",
|
|
48
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Export validation and backup cleanup utilities."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .exceptions import SecurityError
|
|
7
|
+
from .logging import log_security_event
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_export_size(file_path: Path, max_size_mb: int = 100) -> None:
|
|
11
|
+
"""Validate that an export file isn't too large.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path: Path to the export file
|
|
15
|
+
max_size_mb: Maximum allowed size in MB
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
SecurityError: If file is too large
|
|
19
|
+
"""
|
|
20
|
+
if not file_path.exists():
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
file_size_mb = file_path.stat().st_size / (1024 * 1024)
|
|
24
|
+
|
|
25
|
+
if file_size_mb > max_size_mb:
|
|
26
|
+
log_security_event(
|
|
27
|
+
"large_export_detected",
|
|
28
|
+
{
|
|
29
|
+
"path": str(file_path),
|
|
30
|
+
"size_mb": file_size_mb,
|
|
31
|
+
"max_size_mb": max_size_mb,
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
raise SecurityError(
|
|
35
|
+
f"Export file too large: {file_size_mb:.1f}MB > {max_size_mb}MB"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cleanup_old_backups(backup_dir: Path, retention_days: int = 30) -> int:
|
|
40
|
+
"""Clean up old backup files for security.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
backup_dir: Directory containing backup files
|
|
44
|
+
retention_days: Number of days to retain backups
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Number of files cleaned up
|
|
48
|
+
"""
|
|
49
|
+
if not backup_dir.exists():
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
cutoff_time = time.time() - (retention_days * 24 * 60 * 60)
|
|
53
|
+
cleaned_count = 0
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
for backup_file in backup_dir.glob("*.backup*"):
|
|
57
|
+
if backup_file.stat().st_mtime < cutoff_time:
|
|
58
|
+
backup_file.unlink()
|
|
59
|
+
cleaned_count += 1
|
|
60
|
+
|
|
61
|
+
log_security_event(
|
|
62
|
+
"backup_cleaned",
|
|
63
|
+
{
|
|
64
|
+
"path": str(backup_file),
|
|
65
|
+
"age_days": (time.time() - backup_file.stat().st_mtime)
|
|
66
|
+
/ (24 * 60 * 60),
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if cleaned_count > 0:
|
|
71
|
+
log_security_event(
|
|
72
|
+
"backup_cleanup_completed",
|
|
73
|
+
{"files_cleaned": cleaned_count, "retention_days": retention_days},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
log_security_event("backup_cleanup_failed", {"error": str(e)})
|
|
78
|
+
|
|
79
|
+
return cleaned_count
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Secure file and directory operations."""
|
|
2
|
+
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..utils.file_utils import ensure_directory_exists
|
|
7
|
+
from .exceptions import SecurityError
|
|
8
|
+
from .logging import log_security_event
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def create_secure_file(
|
|
13
|
+
path: str | Path, mode: str = "w", permissions: int = 0o600, **kwargs
|
|
14
|
+
):
|
|
15
|
+
"""Create a file with secure permissions as a context manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
path: Path to the file to create
|
|
19
|
+
mode: File open mode (default: 'w')
|
|
20
|
+
permissions: File permissions (default: owner read/write only)
|
|
21
|
+
**kwargs: Additional arguments passed to open()
|
|
22
|
+
|
|
23
|
+
Yields:
|
|
24
|
+
File object
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
SecurityError: If file creation fails or permissions cannot be set
|
|
28
|
+
"""
|
|
29
|
+
path = Path(path)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Create parent directories if they don't exist
|
|
33
|
+
ensure_directory_exists(path.parent)
|
|
34
|
+
|
|
35
|
+
# Open file with specified mode
|
|
36
|
+
with open(path, mode, **kwargs) as f:
|
|
37
|
+
# Set secure permissions after file is created
|
|
38
|
+
try:
|
|
39
|
+
path.chmod(permissions)
|
|
40
|
+
except (OSError, PermissionError) as e:
|
|
41
|
+
# Log warning but don't fail - permissions might not be changeable
|
|
42
|
+
log_security_event(
|
|
43
|
+
"permission_warning", {"path": str(path), "error": str(e)}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
log_security_event(
|
|
47
|
+
"file_created", {"path": str(path), "permissions": oct(permissions)}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
yield f
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
log_security_event("file_creation_failed", {"path": str(path), "error": str(e)})
|
|
54
|
+
raise SecurityError(f"Failed to create secure file {path}: {e}") from e
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_secure_directory(path: Path, permissions: int = 0o700) -> None:
|
|
58
|
+
"""Create a directory with secure permissions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
path: Path to the directory to create
|
|
62
|
+
permissions: Directory permissions (default: owner only)
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
SecurityError: If directory creation fails
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
ensure_directory_exists(path, permissions=permissions)
|
|
69
|
+
# Note: ensure_directory_exists already sets permissions
|
|
70
|
+
|
|
71
|
+
log_security_event(
|
|
72
|
+
"directory_created", {"path": str(path), "permissions": oct(permissions)}
|
|
73
|
+
)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
log_security_event(
|
|
76
|
+
"directory_creation_failed", {"path": str(path), "error": str(e)}
|
|
77
|
+
)
|
|
78
|
+
raise SecurityError(f"Failed to create secure directory {path}: {e}") from e
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def secure_file_permissions(path: Path, permissions: int = 0o600) -> None:
|
|
82
|
+
"""Set secure permissions on an existing file.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
path: Path to the file
|
|
86
|
+
permissions: Permissions to set (default: owner read/write only)
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
SecurityError: If permission setting fails
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
if not path.exists():
|
|
93
|
+
raise SecurityError(f"File does not exist: {path}")
|
|
94
|
+
|
|
95
|
+
path.chmod(permissions)
|
|
96
|
+
|
|
97
|
+
log_security_event(
|
|
98
|
+
"permissions_set", {"path": str(path), "permissions": oct(permissions)}
|
|
99
|
+
)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
log_security_event(
|
|
102
|
+
"permission_setting_failed", {"path": str(path), "error": str(e)}
|
|
103
|
+
)
|
|
104
|
+
raise SecurityError(f"Failed to set secure permissions on {path}: {e}") from e
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Filename sanitization utilities."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .exceptions import SecurityError
|
|
7
|
+
from .logging import log_security_event
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sanitize_filename(filename: str, max_length: int = 255) -> str:
|
|
11
|
+
"""Sanitize a filename to prevent security issues.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
filename: Original filename
|
|
15
|
+
max_length: Maximum allowed filename length
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Sanitized filename
|
|
19
|
+
"""
|
|
20
|
+
if not filename:
|
|
21
|
+
raise SecurityError("Filename cannot be empty")
|
|
22
|
+
|
|
23
|
+
# Remove or replace dangerous characters
|
|
24
|
+
# Windows: < > : " | ? * \0
|
|
25
|
+
# Unix: \0 /
|
|
26
|
+
# Generally dangerous: .. (relative path)
|
|
27
|
+
|
|
28
|
+
# Replace dangerous characters with underscores
|
|
29
|
+
sanitized = re.sub(r'[<>:"/\\|?*\0]', "_", filename)
|
|
30
|
+
|
|
31
|
+
# Replace relative path attempts
|
|
32
|
+
sanitized = sanitized.replace("..", "_")
|
|
33
|
+
|
|
34
|
+
# Remove leading/trailing whitespace and dots
|
|
35
|
+
sanitized = sanitized.strip(" .")
|
|
36
|
+
|
|
37
|
+
# Truncate to max length
|
|
38
|
+
if len(sanitized) > max_length:
|
|
39
|
+
name, ext = os.path.splitext(sanitized)
|
|
40
|
+
if ext:
|
|
41
|
+
max_name_length = max_length - len(ext)
|
|
42
|
+
sanitized = name[:max_name_length] + ext
|
|
43
|
+
else:
|
|
44
|
+
sanitized = sanitized[:max_length]
|
|
45
|
+
|
|
46
|
+
# Ensure we still have a valid filename
|
|
47
|
+
if not sanitized or sanitized in {".", ".."}:
|
|
48
|
+
sanitized = "safe_filename"
|
|
49
|
+
|
|
50
|
+
log_security_event(
|
|
51
|
+
"filename_sanitized", {"original": filename, "sanitized": sanitized}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return sanitized
|