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,480 @@
|
|
|
1
|
+
"""Timezone utilities for the roadmap CLI tool.
|
|
2
|
+
|
|
3
|
+
This module provides timezone-aware datetime handling utilities to support
|
|
4
|
+
distributed teams across multiple timezones. All datetime objects should be
|
|
5
|
+
stored in UTC and displayed in the user's local timezone.
|
|
6
|
+
|
|
7
|
+
Key Principles:
|
|
8
|
+
1. Store all datetimes in UTC
|
|
9
|
+
2. Parse user input in their timezone, convert to UTC for storage
|
|
10
|
+
3. Display datetimes in user's preferred timezone
|
|
11
|
+
4. Maintain backward compatibility during migration
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import UTC, datetime, tzinfo
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from structlog import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger()
|
|
23
|
+
|
|
24
|
+
# Import zoneinfo with fallback for older Python versions
|
|
25
|
+
try:
|
|
26
|
+
from zoneinfo import ZoneInfo # type: ignore[attr-defined]
|
|
27
|
+
|
|
28
|
+
ZONEINFO_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
# Fallback for Python < 3.9
|
|
31
|
+
try:
|
|
32
|
+
from backports.zoneinfo import ZoneInfo # type: ignore
|
|
33
|
+
|
|
34
|
+
ZONEINFO_AVAILABLE = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
ZONEINFO_AVAILABLE = False
|
|
37
|
+
# Use basic timezone handling as fallback
|
|
38
|
+
|
|
39
|
+
class ZoneInfo(tzinfo):
|
|
40
|
+
"""Minimal ZoneInfo fallback for older Python versions."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, name: str):
|
|
43
|
+
"""Initialize ZoneInfo fallback.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Timezone name.
|
|
47
|
+
"""
|
|
48
|
+
self.name = name
|
|
49
|
+
if name == "UTC":
|
|
50
|
+
self._tz = UTC
|
|
51
|
+
else:
|
|
52
|
+
# For other timezones, we'll use UTC as fallback
|
|
53
|
+
# This is not ideal but maintains basic functionality
|
|
54
|
+
self._tz = UTC
|
|
55
|
+
|
|
56
|
+
def utcoffset(self, dt: datetime | None) -> Any:
|
|
57
|
+
"""Get the UTC offset for this timezone.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
dt: The datetime to get the offset for.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The UTC offset as a timedelta.
|
|
64
|
+
"""
|
|
65
|
+
return self._tz.utcoffset(dt)
|
|
66
|
+
|
|
67
|
+
def tzname(self, dt: datetime | None) -> str | None:
|
|
68
|
+
"""Get the timezone name.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
dt: The datetime to get the timezone name for.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The timezone name as a string.
|
|
75
|
+
"""
|
|
76
|
+
return self._tz.tzname(dt)
|
|
77
|
+
|
|
78
|
+
def dst(self, dt: datetime | None) -> Any:
|
|
79
|
+
"""Get the daylight saving time offset.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
dt: The datetime to get the DST offset for.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
The DST offset as a timedelta, or None if not applicable.
|
|
86
|
+
"""
|
|
87
|
+
return self._tz.dst(dt)
|
|
88
|
+
|
|
89
|
+
def __repr__(self):
|
|
90
|
+
"""Return string representation of ZoneInfo."""
|
|
91
|
+
return f"ZoneInfo('{self.name}')"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TimezoneManager:
|
|
95
|
+
"""Manages timezone operations for the roadmap CLI."""
|
|
96
|
+
|
|
97
|
+
DEFAULT_TIMEZONE = "UTC"
|
|
98
|
+
|
|
99
|
+
def __init__(self, user_timezone: str | None = None):
|
|
100
|
+
"""Initialize timezone manager with user's preferred timezone."""
|
|
101
|
+
self.user_timezone = user_timezone or self._detect_system_timezone()
|
|
102
|
+
self._validate_timezone(self.user_timezone)
|
|
103
|
+
|
|
104
|
+
def _get_tz_from_env(self) -> str | None:
|
|
105
|
+
"""Get timezone from TZ environment variable.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Timezone name or None
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
tz_env = os.environ.get("TZ")
|
|
112
|
+
if tz_env:
|
|
113
|
+
ZoneInfo(tz_env) # Validate it
|
|
114
|
+
return tz_env
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.debug(
|
|
117
|
+
"failed_to_validate_tz_env", error=str(e), severity="operational"
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def _get_tz_from_etc_timezone(self) -> str | None:
|
|
122
|
+
"""Get timezone from /etc/timezone file.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Timezone name or None
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
timezone_file = Path("/etc/timezone")
|
|
129
|
+
if timezone_file.exists():
|
|
130
|
+
tz_name = timezone_file.read_text().strip()
|
|
131
|
+
ZoneInfo(tz_name) # Validate it
|
|
132
|
+
return tz_name
|
|
133
|
+
except (OSError, FileNotFoundError, Exception):
|
|
134
|
+
pass
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def _get_tz_from_localtime_symlink(self) -> str | None:
|
|
138
|
+
"""Get timezone from /etc/localtime symlink.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Timezone name or None
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
localtime_path = Path("/etc/localtime")
|
|
145
|
+
if localtime_path.is_symlink():
|
|
146
|
+
target = localtime_path.readlink()
|
|
147
|
+
# Extract timezone from path like /usr/share/zoneinfo/America/New_York
|
|
148
|
+
if "zoneinfo" in str(target):
|
|
149
|
+
tz_parts = str(target).split("zoneinfo/")
|
|
150
|
+
if len(tz_parts) > 1:
|
|
151
|
+
tz_name = tz_parts[1]
|
|
152
|
+
ZoneInfo(tz_name) # Validate it
|
|
153
|
+
return tz_name
|
|
154
|
+
except (OSError, Exception):
|
|
155
|
+
pass
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _detect_system_timezone(self) -> str:
|
|
159
|
+
"""Detect the system's timezone."""
|
|
160
|
+
if ZONEINFO_AVAILABLE:
|
|
161
|
+
# Try TZ environment variable
|
|
162
|
+
tz = self._get_tz_from_env()
|
|
163
|
+
if tz:
|
|
164
|
+
return tz
|
|
165
|
+
|
|
166
|
+
# Only Unix-like systems
|
|
167
|
+
if sys.platform != "win32":
|
|
168
|
+
# Check /etc/timezone
|
|
169
|
+
tz = self._get_tz_from_etc_timezone()
|
|
170
|
+
if tz:
|
|
171
|
+
return tz
|
|
172
|
+
|
|
173
|
+
# Check /etc/localtime symlink
|
|
174
|
+
tz = self._get_tz_from_localtime_symlink()
|
|
175
|
+
if tz:
|
|
176
|
+
return tz
|
|
177
|
+
|
|
178
|
+
# Fallback to UTC if detection fails
|
|
179
|
+
return self.DEFAULT_TIMEZONE
|
|
180
|
+
|
|
181
|
+
def _validate_timezone(self, tz_name: str) -> None:
|
|
182
|
+
"""Validate that the timezone name is valid."""
|
|
183
|
+
try:
|
|
184
|
+
ZoneInfo(tz_name)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise ValueError(f"Invalid timezone '{tz_name}': {e}") from e
|
|
187
|
+
|
|
188
|
+
def now_utc(self) -> datetime:
|
|
189
|
+
"""Get current datetime in UTC with timezone information."""
|
|
190
|
+
return datetime.now(UTC)
|
|
191
|
+
|
|
192
|
+
def now_local(self) -> datetime:
|
|
193
|
+
"""Get current datetime in user's timezone."""
|
|
194
|
+
return self.now_utc().astimezone(ZoneInfo(self.user_timezone))
|
|
195
|
+
|
|
196
|
+
def parse_user_input(
|
|
197
|
+
self, date_str: str, input_timezone: str | None = None
|
|
198
|
+
) -> datetime:
|
|
199
|
+
"""Parse user date input and convert to UTC.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
date_str: Date string in various formats
|
|
203
|
+
input_timezone: Timezone to assume for the input (defaults to user timezone)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Timezone-aware datetime in UTC
|
|
207
|
+
"""
|
|
208
|
+
input_tz = input_timezone or self.user_timezone
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
# Try to parse as ISO format first
|
|
212
|
+
if "T" in date_str:
|
|
213
|
+
# Handle ISO format: 2025-12-31T23:59:59
|
|
214
|
+
if date_str.endswith("Z"):
|
|
215
|
+
# Already UTC
|
|
216
|
+
return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
|
217
|
+
elif "+" in date_str or date_str.count("-") > 2:
|
|
218
|
+
# Has timezone info
|
|
219
|
+
return datetime.fromisoformat(date_str)
|
|
220
|
+
else:
|
|
221
|
+
# No timezone info, assume user timezone
|
|
222
|
+
naive_dt = datetime.fromisoformat(date_str)
|
|
223
|
+
return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(UTC)
|
|
224
|
+
else:
|
|
225
|
+
# Try to parse as date only: 2025-12-31
|
|
226
|
+
if date_str.count("-") == 2:
|
|
227
|
+
naive_dt = datetime.strptime(date_str, "%Y-%m-%d")
|
|
228
|
+
return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(UTC)
|
|
229
|
+
|
|
230
|
+
# Try other common formats
|
|
231
|
+
formats = [
|
|
232
|
+
"%Y-%m-%d %H:%M:%S",
|
|
233
|
+
"%Y/%m/%d %H:%M:%S",
|
|
234
|
+
"%Y-%m-%d %H:%M",
|
|
235
|
+
"%Y/%m/%d %H:%M",
|
|
236
|
+
"%m/%d/%Y %H:%M:%S",
|
|
237
|
+
"%m/%d/%Y %H:%M",
|
|
238
|
+
"%m/%d/%Y",
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
for fmt in formats:
|
|
242
|
+
try:
|
|
243
|
+
naive_dt = datetime.strptime(date_str, fmt)
|
|
244
|
+
return naive_dt.replace(tzinfo=ZoneInfo(input_tz)).astimezone(
|
|
245
|
+
UTC
|
|
246
|
+
)
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
logger.debug(
|
|
249
|
+
"date_format_failed",
|
|
250
|
+
format_pattern=fmt,
|
|
251
|
+
input=date_str,
|
|
252
|
+
error=str(e),
|
|
253
|
+
)
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
raise ValueError(f"Unable to parse date string: {date_str}")
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
raise ValueError(f"Invalid date format '{date_str}': {e}") from e
|
|
260
|
+
|
|
261
|
+
def format_for_user(self, dt: datetime, format_str: str | None = None) -> str:
|
|
262
|
+
"""Format datetime for display to user in their timezone.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
dt: Timezone-aware datetime (preferably UTC)
|
|
266
|
+
format_str: strftime format string
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Formatted datetime string in user's timezone
|
|
270
|
+
"""
|
|
271
|
+
if dt.tzinfo is None:
|
|
272
|
+
# Handle naive datetimes by assuming UTC
|
|
273
|
+
dt = dt.replace(tzinfo=UTC)
|
|
274
|
+
|
|
275
|
+
# Convert to user's timezone
|
|
276
|
+
local_dt = dt.astimezone(ZoneInfo(self.user_timezone))
|
|
277
|
+
|
|
278
|
+
# Default format includes timezone
|
|
279
|
+
if format_str is None:
|
|
280
|
+
format_str = "%Y-%m-%d %H:%M:%S %Z"
|
|
281
|
+
|
|
282
|
+
return local_dt.strftime(format_str)
|
|
283
|
+
|
|
284
|
+
def _format_relative_seconds(
|
|
285
|
+
self, total_seconds: float, diff_seconds: float
|
|
286
|
+
) -> str:
|
|
287
|
+
"""Format relative time in seconds.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
total_seconds: Absolute seconds difference
|
|
291
|
+
diff_seconds: Signed seconds difference
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Formatted string
|
|
295
|
+
"""
|
|
296
|
+
if total_seconds < 60:
|
|
297
|
+
return "just now"
|
|
298
|
+
elif total_seconds < 3600: # Less than 1 hour
|
|
299
|
+
minutes = int(total_seconds // 60)
|
|
300
|
+
unit = "minute" if minutes == 1 else "minutes"
|
|
301
|
+
direction = "ago" if diff_seconds < 0 else "from now"
|
|
302
|
+
return f"{minutes} {unit} {direction}"
|
|
303
|
+
elif total_seconds < 86400: # Less than 1 day
|
|
304
|
+
hours = int(total_seconds // 3600)
|
|
305
|
+
unit = "hour" if hours == 1 else "hours"
|
|
306
|
+
direction = "ago" if diff_seconds < 0 else "from now"
|
|
307
|
+
return f"{hours} {unit} {direction}"
|
|
308
|
+
else: # Days
|
|
309
|
+
days = int(total_seconds // 86400)
|
|
310
|
+
unit = "day" if days == 1 else "days"
|
|
311
|
+
direction = "ago" if diff_seconds < 0 else "from now"
|
|
312
|
+
return f"{days} {unit} {direction}"
|
|
313
|
+
|
|
314
|
+
def format_relative(self, dt: datetime) -> str:
|
|
315
|
+
"""Format datetime as relative time (e.g., '2 hours ago', 'in 3 days').
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
dt: Timezone-aware datetime
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Human-readable relative time string
|
|
322
|
+
"""
|
|
323
|
+
if dt.tzinfo is None:
|
|
324
|
+
dt = dt.replace(tzinfo=UTC)
|
|
325
|
+
|
|
326
|
+
now = self.now_utc()
|
|
327
|
+
diff = dt - now
|
|
328
|
+
|
|
329
|
+
# Calculate time difference
|
|
330
|
+
total_seconds = abs(diff.total_seconds())
|
|
331
|
+
return self._format_relative_seconds(total_seconds, diff.total_seconds())
|
|
332
|
+
|
|
333
|
+
def is_timezone_aware(self, dt: datetime) -> bool:
|
|
334
|
+
"""Check if datetime is timezone-aware."""
|
|
335
|
+
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
|
336
|
+
|
|
337
|
+
def make_aware(self, dt: datetime, tz: str | None = None) -> datetime:
|
|
338
|
+
"""Convert naive datetime to timezone-aware.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
dt: Naive datetime
|
|
342
|
+
tz: Timezone to assign (defaults to user timezone)
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Timezone-aware datetime in UTC
|
|
346
|
+
"""
|
|
347
|
+
if self.is_timezone_aware(dt):
|
|
348
|
+
return dt.astimezone(UTC)
|
|
349
|
+
|
|
350
|
+
assumed_tz = tz or self.user_timezone
|
|
351
|
+
return dt.replace(tzinfo=ZoneInfo(assumed_tz)).astimezone(UTC)
|
|
352
|
+
|
|
353
|
+
def get_common_timezones(self) -> list:
|
|
354
|
+
"""Get list of common timezone names."""
|
|
355
|
+
if ZONEINFO_AVAILABLE:
|
|
356
|
+
try:
|
|
357
|
+
from zoneinfo import available_timezones
|
|
358
|
+
|
|
359
|
+
# Return a curated list of common timezones
|
|
360
|
+
common = [
|
|
361
|
+
"UTC",
|
|
362
|
+
"US/Eastern",
|
|
363
|
+
"US/Central",
|
|
364
|
+
"US/Mountain",
|
|
365
|
+
"US/Pacific",
|
|
366
|
+
"Europe/London",
|
|
367
|
+
"Europe/Paris",
|
|
368
|
+
"Europe/Berlin",
|
|
369
|
+
"Asia/Tokyo",
|
|
370
|
+
"Asia/Shanghai",
|
|
371
|
+
"Asia/Kolkata",
|
|
372
|
+
"Australia/Sydney",
|
|
373
|
+
"Australia/Melbourne",
|
|
374
|
+
"America/New_York",
|
|
375
|
+
"America/Chicago",
|
|
376
|
+
"America/Denver",
|
|
377
|
+
"America/Los_Angeles",
|
|
378
|
+
"America/Toronto",
|
|
379
|
+
"America/Mexico_City",
|
|
380
|
+
"America/Sao_Paulo",
|
|
381
|
+
]
|
|
382
|
+
# Filter to only include available timezones
|
|
383
|
+
available = available_timezones()
|
|
384
|
+
return [tz for tz in common if tz in available]
|
|
385
|
+
except ImportError:
|
|
386
|
+
logger.debug("zoneinfo_import_failed", severity="operational")
|
|
387
|
+
|
|
388
|
+
# Fallback list for systems without zoneinfo
|
|
389
|
+
return [
|
|
390
|
+
"UTC",
|
|
391
|
+
"US/Eastern",
|
|
392
|
+
"US/Central",
|
|
393
|
+
"US/Mountain",
|
|
394
|
+
"US/Pacific",
|
|
395
|
+
"Europe/London",
|
|
396
|
+
"Europe/Paris",
|
|
397
|
+
"Europe/Berlin",
|
|
398
|
+
"Asia/Tokyo",
|
|
399
|
+
"Asia/Shanghai",
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# Global timezone manager instance
|
|
404
|
+
_global_timezone_manager: TimezoneManager | None = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_timezone_manager(user_timezone: str | None = None) -> TimezoneManager:
|
|
408
|
+
"""Get or create the global timezone manager instance."""
|
|
409
|
+
global _global_timezone_manager
|
|
410
|
+
|
|
411
|
+
if _global_timezone_manager is None or user_timezone is not None:
|
|
412
|
+
_global_timezone_manager = TimezoneManager(user_timezone)
|
|
413
|
+
|
|
414
|
+
return _global_timezone_manager
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def now_utc() -> datetime:
|
|
418
|
+
"""Get current UTC datetime."""
|
|
419
|
+
return get_timezone_manager().now_utc()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def now_local(user_timezone: str | None = None) -> datetime:
|
|
423
|
+
"""Get current datetime in user's timezone."""
|
|
424
|
+
return get_timezone_manager(user_timezone).now_local()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def parse_datetime(date_str: str, input_timezone: str | None = None) -> datetime:
|
|
428
|
+
"""Parse user-input datetime string."""
|
|
429
|
+
return get_timezone_manager().parse_user_input(date_str, input_timezone)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def format_datetime(
|
|
433
|
+
dt: datetime, format_str: str | None = None, user_timezone: str | None = None
|
|
434
|
+
) -> str:
|
|
435
|
+
"""Format datetime for user display."""
|
|
436
|
+
return get_timezone_manager(user_timezone).format_for_user(dt, format_str)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def format_relative_time(dt: datetime, user_timezone: str | None = None) -> str:
|
|
440
|
+
"""Format relative time representation."""
|
|
441
|
+
return get_timezone_manager(user_timezone).format_relative(dt)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def migrate_naive_datetime(dt: datetime, assumed_timezone: str = "UTC") -> datetime:
|
|
445
|
+
"""Migrate a naive datetime to timezone-aware UTC.
|
|
446
|
+
|
|
447
|
+
This function is used during the migration process to convert existing
|
|
448
|
+
naive datetimes to timezone-aware datetimes.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
dt: Naive datetime to migrate
|
|
452
|
+
assumed_timezone: Timezone to assume for the naive datetime
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Timezone-aware datetime in UTC
|
|
456
|
+
"""
|
|
457
|
+
if dt.tzinfo is not None:
|
|
458
|
+
# Already timezone-aware, convert to UTC
|
|
459
|
+
return dt.astimezone(UTC)
|
|
460
|
+
|
|
461
|
+
# Assume the specified timezone and convert to UTC
|
|
462
|
+
return dt.replace(tzinfo=ZoneInfo(assumed_timezone)).astimezone(UTC)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def ensure_timezone_aware(dt: datetime, assumed_timezone: str = "UTC") -> datetime:
|
|
466
|
+
"""Ensure a datetime is timezone-aware, converting naive datetimes as needed.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
dt: Datetime that may be naive or timezone-aware
|
|
470
|
+
assumed_timezone: Timezone to assume for naive datetimes
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
Timezone-aware datetime in UTC
|
|
474
|
+
"""
|
|
475
|
+
if dt.tzinfo is not None:
|
|
476
|
+
# Already timezone-aware, convert to UTC for consistency
|
|
477
|
+
return dt.astimezone(UTC)
|
|
478
|
+
|
|
479
|
+
# Naive datetime - assume specified timezone and convert to UTC
|
|
480
|
+
return dt.replace(tzinfo=ZoneInfo(assumed_timezone)).astimezone(UTC)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Unified Validation Framework for Roadmap CLI.
|
|
2
|
+
|
|
3
|
+
This module provides centralized validation utilities, field validators, and consistent
|
|
4
|
+
validation error handling to eliminate duplicate validation patterns across the codebase.
|
|
5
|
+
|
|
6
|
+
Key Features:
|
|
7
|
+
- Centralized field validation for common data types
|
|
8
|
+
- Reusable validation patterns for issues, milestones, and projects
|
|
9
|
+
- Consistent validation error reporting
|
|
10
|
+
- Extensible validation framework for custom validators
|
|
11
|
+
- Integration with error handling framework
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from roadmap.common.validation.field_validator import FieldValidator
|
|
15
|
+
from roadmap.common.validation.result import ValidationResult
|
|
16
|
+
from roadmap.common.validation.roadmap_validator import RoadmapValidator
|
|
17
|
+
from roadmap.common.validation.schema_validator import SchemaValidator
|
|
18
|
+
from roadmap.common.validation.validators import (
|
|
19
|
+
ValidationType,
|
|
20
|
+
validate_and_raise,
|
|
21
|
+
validate_frontmatter_structure,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ValidationResult",
|
|
26
|
+
"FieldValidator",
|
|
27
|
+
"SchemaValidator",
|
|
28
|
+
"RoadmapValidator",
|
|
29
|
+
"ValidationType",
|
|
30
|
+
"validate_and_raise",
|
|
31
|
+
"validate_frontmatter_structure",
|
|
32
|
+
# Global instance for backward compatibility
|
|
33
|
+
"default_validator",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Global validator instance
|
|
37
|
+
default_validator = RoadmapValidator()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Individual field validator with configurable rules."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .result import ValidationResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FieldValidator:
|
|
11
|
+
"""Individual field validator with configurable rules."""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
field_name: str,
|
|
16
|
+
required: bool = False,
|
|
17
|
+
min_length: int | None = None,
|
|
18
|
+
max_length: int | None = None,
|
|
19
|
+
pattern: str | None = None,
|
|
20
|
+
enum_values: list[Any] | None = None,
|
|
21
|
+
custom_validator: Callable[[Any], tuple[bool, str]] | None = None,
|
|
22
|
+
allow_none: bool | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize FieldValidator.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
field_name: Name of the field to validate.
|
|
28
|
+
required: Whether the field is required.
|
|
29
|
+
min_length: Minimum length if applicable.
|
|
30
|
+
max_length: Maximum length if applicable.
|
|
31
|
+
pattern: Regex pattern to match against.
|
|
32
|
+
enum_values: List of allowed enum values.
|
|
33
|
+
custom_validator: Optional custom validation function.
|
|
34
|
+
allow_none: Whether None values are allowed.
|
|
35
|
+
"""
|
|
36
|
+
self.field_name = field_name
|
|
37
|
+
self.required = required
|
|
38
|
+
self.min_length = min_length
|
|
39
|
+
self.max_length = max_length
|
|
40
|
+
self.pattern = re.compile(pattern) if pattern else None
|
|
41
|
+
self.enum_values = enum_values
|
|
42
|
+
self.custom_validator = custom_validator
|
|
43
|
+
# If allow_none is not specified, set it based on required status
|
|
44
|
+
self.allow_none = allow_none if allow_none is not None else not required
|
|
45
|
+
|
|
46
|
+
def validate(self, value: Any) -> ValidationResult:
|
|
47
|
+
"""Validate a field value."""
|
|
48
|
+
result = ValidationResult(field=self.field_name)
|
|
49
|
+
|
|
50
|
+
# Check if value is None first
|
|
51
|
+
if value is None:
|
|
52
|
+
if self.required and not self.allow_none:
|
|
53
|
+
result.add_error(f"Field '{self.field_name}' is required")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
# Apply all validation rules
|
|
57
|
+
self._validate_required(value, result)
|
|
58
|
+
self._validate_enum(value, result)
|
|
59
|
+
self._validate_pattern(value, result)
|
|
60
|
+
self._validate_length(value, result)
|
|
61
|
+
self._validate_custom(value, result)
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def _validate_required(self, value: Any, result: ValidationResult) -> None:
|
|
66
|
+
"""Validate that required field is not empty."""
|
|
67
|
+
if self.required and (value is None or value == ""):
|
|
68
|
+
result.add_error(f"Field '{self.field_name}' is required")
|
|
69
|
+
|
|
70
|
+
def _validate_enum(self, value: Any, result: ValidationResult) -> None:
|
|
71
|
+
"""Validate that value is one of allowed enum values."""
|
|
72
|
+
if self.enum_values is not None and value not in self.enum_values:
|
|
73
|
+
result.add_error(
|
|
74
|
+
f"Field '{self.field_name}' must be one of: "
|
|
75
|
+
f"{', '.join(str(v) for v in self.enum_values)}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def _validate_pattern(self, value: Any, result: ValidationResult) -> None:
|
|
79
|
+
"""Validate that string value matches pattern."""
|
|
80
|
+
if self.pattern is not None and isinstance(value, str):
|
|
81
|
+
if not self.pattern.match(value):
|
|
82
|
+
result.add_error(f"Field '{self.field_name}' format is invalid")
|
|
83
|
+
|
|
84
|
+
def _validate_length(self, value: Any, result: ValidationResult) -> None:
|
|
85
|
+
"""Validate string length constraints."""
|
|
86
|
+
if not isinstance(value, str):
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if self.min_length is not None and len(value) < self.min_length:
|
|
90
|
+
result.add_error(
|
|
91
|
+
f"Field '{self.field_name}' must be at least {self.min_length} characters"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if self.max_length is not None and len(value) > self.max_length:
|
|
95
|
+
result.add_error(
|
|
96
|
+
f"Field '{self.field_name}' must be no more than {self.max_length} characters"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _validate_custom(self, value: Any, result: ValidationResult) -> None:
|
|
100
|
+
"""Validate using custom validator function."""
|
|
101
|
+
if self.custom_validator is not None:
|
|
102
|
+
is_valid, error_msg = self.custom_validator(value)
|
|
103
|
+
if not is_valid:
|
|
104
|
+
result.add_error(f"Field '{self.field_name}' {error_msg}")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Validation result tracking and error aggregation."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ValidationResult:
|
|
5
|
+
"""Result of a validation operation."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
is_valid: bool = True,
|
|
10
|
+
errors: list[str] | None = None,
|
|
11
|
+
field: str | None = None,
|
|
12
|
+
):
|
|
13
|
+
"""Initialize ValidationResult.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
is_valid: Whether validation passed.
|
|
17
|
+
errors: List of validation error messages.
|
|
18
|
+
field: Name of the field being validated.
|
|
19
|
+
"""
|
|
20
|
+
self.is_valid = is_valid
|
|
21
|
+
self.errors = errors or []
|
|
22
|
+
self.field = field
|
|
23
|
+
|
|
24
|
+
def add_error(self, error: str):
|
|
25
|
+
"""Add an error to the validation result."""
|
|
26
|
+
self.errors.append(error)
|
|
27
|
+
self.is_valid = False
|
|
28
|
+
|
|
29
|
+
def merge(self, other: "ValidationResult"):
|
|
30
|
+
"""Merge another validation result into this one."""
|
|
31
|
+
if not other.is_valid:
|
|
32
|
+
self.is_valid = False
|
|
33
|
+
self.errors.extend(other.errors)
|
|
34
|
+
|
|
35
|
+
def __bool__(self) -> bool:
|
|
36
|
+
"""Return whether validation passed."""
|
|
37
|
+
return self.is_valid
|