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.
Files changed (521) hide show
  1. roadmap/__init__.py +27 -0
  2. roadmap/adapters/__init__.py +1 -0
  3. roadmap/adapters/base_paginated_adapter.py +117 -0
  4. roadmap/adapters/cli/__init__.py +249 -0
  5. roadmap/adapters/cli/analysis/__init__.py +5 -0
  6. roadmap/adapters/cli/analysis/commands.py +370 -0
  7. roadmap/adapters/cli/analysis/presenter.py +125 -0
  8. roadmap/adapters/cli/archive_operations.py +86 -0
  9. roadmap/adapters/cli/cli_command_helpers.py +109 -0
  10. roadmap/adapters/cli/cli_confirmations.py +74 -0
  11. roadmap/adapters/cli/cli_error_handlers.py +146 -0
  12. roadmap/adapters/cli/cli_validators.py +39 -0
  13. roadmap/adapters/cli/commands/sync_status.py +308 -0
  14. roadmap/adapters/cli/comment/__init__.py +5 -0
  15. roadmap/adapters/cli/comment/commands.py +106 -0
  16. roadmap/adapters/cli/config/__init__.py +5 -0
  17. roadmap/adapters/cli/config/commands.py +198 -0
  18. roadmap/adapters/cli/console_exports.py +10 -0
  19. roadmap/adapters/cli/core.py +26 -0
  20. roadmap/adapters/cli/crud/__init__.py +47 -0
  21. roadmap/adapters/cli/crud/base_archive.py +208 -0
  22. roadmap/adapters/cli/crud/base_create.py +154 -0
  23. roadmap/adapters/cli/crud/base_delete.py +155 -0
  24. roadmap/adapters/cli/crud/base_restore.py +283 -0
  25. roadmap/adapters/cli/crud/base_update.py +193 -0
  26. roadmap/adapters/cli/crud/crud_helpers.py +206 -0
  27. roadmap/adapters/cli/crud/crud_utils.py +152 -0
  28. roadmap/adapters/cli/crud/entity_builders.py +374 -0
  29. roadmap/adapters/cli/data/__init__.py +8 -0
  30. roadmap/adapters/cli/data/commands.py +149 -0
  31. roadmap/adapters/cli/decorators.py +208 -0
  32. roadmap/adapters/cli/dtos/__init__.py +155 -0
  33. roadmap/adapters/cli/exception_handler.py +105 -0
  34. roadmap/adapters/cli/git/__init__.py +8 -0
  35. roadmap/adapters/cli/git/commands.py +475 -0
  36. roadmap/adapters/cli/git/handlers/__init__.py +1 -0
  37. roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
  38. roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
  39. roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
  40. roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
  41. roadmap/adapters/cli/git/hooks_config.py +327 -0
  42. roadmap/adapters/cli/git/status_display.py +158 -0
  43. roadmap/adapters/cli/health/__init__.py +8 -0
  44. roadmap/adapters/cli/health/db_integrity.py +204 -0
  45. roadmap/adapters/cli/health/enhancer.py +227 -0
  46. roadmap/adapters/cli/health/fixer.py +295 -0
  47. roadmap/adapters/cli/health/fixers/__init__.py +10 -0
  48. roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
  49. roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
  50. roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
  51. roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
  52. roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
  53. roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
  54. roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
  55. roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
  56. roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
  57. roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
  58. roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
  59. roadmap/adapters/cli/health/formatter.py +250 -0
  60. roadmap/adapters/cli/health/formatters.py +427 -0
  61. roadmap/adapters/cli/health/scan.py +293 -0
  62. roadmap/adapters/cli/init/__init__.py +5 -0
  63. roadmap/adapters/cli/init/commands.py +587 -0
  64. roadmap/adapters/cli/init.py +9 -0
  65. roadmap/adapters/cli/issues/__init__.py +70 -0
  66. roadmap/adapters/cli/issues/archive.py +137 -0
  67. roadmap/adapters/cli/issues/archive_class.py +253 -0
  68. roadmap/adapters/cli/issues/block.py +39 -0
  69. roadmap/adapters/cli/issues/close.py +216 -0
  70. roadmap/adapters/cli/issues/comment.py +178 -0
  71. roadmap/adapters/cli/issues/create.py +162 -0
  72. roadmap/adapters/cli/issues/delete.py +31 -0
  73. roadmap/adapters/cli/issues/deps.py +161 -0
  74. roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
  75. roadmap/adapters/cli/issues/link.py +308 -0
  76. roadmap/adapters/cli/issues/list.py +402 -0
  77. roadmap/adapters/cli/issues/lookup.py +96 -0
  78. roadmap/adapters/cli/issues/progress.py +102 -0
  79. roadmap/adapters/cli/issues/restore.py +96 -0
  80. roadmap/adapters/cli/issues/restore_class.py +57 -0
  81. roadmap/adapters/cli/issues/start.py +165 -0
  82. roadmap/adapters/cli/issues/sync_status.py +374 -0
  83. roadmap/adapters/cli/issues/unblock.py +43 -0
  84. roadmap/adapters/cli/issues/unlink.py +104 -0
  85. roadmap/adapters/cli/issues/update.py +120 -0
  86. roadmap/adapters/cli/issues/view.py +36 -0
  87. roadmap/adapters/cli/layout.py +220 -0
  88. roadmap/adapters/cli/mappers.py +200 -0
  89. roadmap/adapters/cli/milestones/__init__.py +52 -0
  90. roadmap/adapters/cli/milestones/archive.py +125 -0
  91. roadmap/adapters/cli/milestones/archive_class.py +236 -0
  92. roadmap/adapters/cli/milestones/assign.py +62 -0
  93. roadmap/adapters/cli/milestones/close.py +177 -0
  94. roadmap/adapters/cli/milestones/create.py +131 -0
  95. roadmap/adapters/cli/milestones/delete.py +27 -0
  96. roadmap/adapters/cli/milestones/kanban.py +123 -0
  97. roadmap/adapters/cli/milestones/list.py +87 -0
  98. roadmap/adapters/cli/milestones/recalculate.py +79 -0
  99. roadmap/adapters/cli/milestones/restore.py +87 -0
  100. roadmap/adapters/cli/milestones/restore_class.py +99 -0
  101. roadmap/adapters/cli/milestones/update.py +61 -0
  102. roadmap/adapters/cli/milestones/view.py +142 -0
  103. roadmap/adapters/cli/output_manager.py +319 -0
  104. roadmap/adapters/cli/presentation/base_presenter.py +121 -0
  105. roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
  106. roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
  107. roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
  108. roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
  109. roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
  110. roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
  111. roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
  112. roadmap/adapters/cli/presentation/project_presenter.py +233 -0
  113. roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
  114. roadmap/adapters/cli/presentation/table_builders.py +95 -0
  115. roadmap/adapters/cli/projects/__init__.py +42 -0
  116. roadmap/adapters/cli/projects/archive.py +127 -0
  117. roadmap/adapters/cli/projects/archive_class.py +96 -0
  118. roadmap/adapters/cli/projects/close.py +94 -0
  119. roadmap/adapters/cli/projects/create.py +50 -0
  120. roadmap/adapters/cli/projects/delete.py +27 -0
  121. roadmap/adapters/cli/projects/list.py +227 -0
  122. roadmap/adapters/cli/projects/restore.py +87 -0
  123. roadmap/adapters/cli/projects/restore_class.py +56 -0
  124. roadmap/adapters/cli/projects/update.py +57 -0
  125. roadmap/adapters/cli/projects/view.py +157 -0
  126. roadmap/adapters/cli/services/__init__.py +1 -0
  127. roadmap/adapters/cli/services/daily_summary_service.py +313 -0
  128. roadmap/adapters/cli/services/export_manager.py +268 -0
  129. roadmap/adapters/cli/services/milestone_list_service.py +266 -0
  130. roadmap/adapters/cli/services/project_initialization_service.py +29 -0
  131. roadmap/adapters/cli/services/project_status_service.py +462 -0
  132. roadmap/adapters/cli/services/sync_service.py +62 -0
  133. roadmap/adapters/cli/status.py +408 -0
  134. roadmap/adapters/cli/styling.py +21 -0
  135. roadmap/adapters/cli/sync.py +1117 -0
  136. roadmap/adapters/cli/sync_context.py +432 -0
  137. roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
  138. roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
  139. roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
  140. roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
  141. roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
  142. roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
  143. roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
  144. roadmap/adapters/cli/sync_metrics_command.py +190 -0
  145. roadmap/adapters/cli/sync_presenter.py +58 -0
  146. roadmap/adapters/cli/sync_validation.py +295 -0
  147. roadmap/adapters/cli/today.py +52 -0
  148. roadmap/adapters/cli/utils/click_options.py +38 -0
  149. roadmap/adapters/git/__init__.py +1 -0
  150. roadmap/adapters/git/git.py +208 -0
  151. roadmap/adapters/git/git_branch_manager.py +400 -0
  152. roadmap/adapters/git/git_command_executor.py +56 -0
  153. roadmap/adapters/git/git_commit_analyzer.py +334 -0
  154. roadmap/adapters/git/git_hooks.py +22 -0
  155. roadmap/adapters/git/git_hooks_manager.py +455 -0
  156. roadmap/adapters/git/git_repository_info.py +65 -0
  157. roadmap/adapters/git/hook_installer.py +106 -0
  158. roadmap/adapters/git/hook_registry.py +97 -0
  159. roadmap/adapters/git/hook_script_generator.py +58 -0
  160. roadmap/adapters/git/sync_monitor.py +409 -0
  161. roadmap/adapters/git/workflow_automation.py +286 -0
  162. roadmap/adapters/github/__init__.py +1 -0
  163. roadmap/adapters/github/github.py +360 -0
  164. roadmap/adapters/github/handlers/__init__.py +17 -0
  165. roadmap/adapters/github/handlers/base.py +337 -0
  166. roadmap/adapters/github/handlers/collaborators.py +165 -0
  167. roadmap/adapters/github/handlers/comments.py +118 -0
  168. roadmap/adapters/github/handlers/issues.py +150 -0
  169. roadmap/adapters/github/handlers/labels.py +164 -0
  170. roadmap/adapters/github/handlers/milestones.py +104 -0
  171. roadmap/adapters/persistence/__init__.py +35 -0
  172. roadmap/adapters/persistence/conflict_resolver.py +190 -0
  173. roadmap/adapters/persistence/database_manager.py +492 -0
  174. roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
  175. roadmap/adapters/persistence/file_locking.py +348 -0
  176. roadmap/adapters/persistence/file_parser.py +92 -0
  177. roadmap/adapters/persistence/file_synchronizer.py +107 -0
  178. roadmap/adapters/persistence/focused_managers.py +140 -0
  179. roadmap/adapters/persistence/git_history.py +330 -0
  180. roadmap/adapters/persistence/parser/__init__.py +20 -0
  181. roadmap/adapters/persistence/parser/frontmatter.py +152 -0
  182. roadmap/adapters/persistence/parser/issue.py +245 -0
  183. roadmap/adapters/persistence/parser/milestone.py +146 -0
  184. roadmap/adapters/persistence/parser/project.py +106 -0
  185. roadmap/adapters/persistence/persistence.py +288 -0
  186. roadmap/adapters/persistence/repositories/__init__.py +15 -0
  187. roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
  188. roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
  189. roadmap/adapters/persistence/repositories/project_repository.py +168 -0
  190. roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
  191. roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
  192. roadmap/adapters/persistence/storage/__init__.py +36 -0
  193. roadmap/adapters/persistence/storage/conflicts.py +101 -0
  194. roadmap/adapters/persistence/storage/connection_manager.py +79 -0
  195. roadmap/adapters/persistence/storage/issue_storage.py +89 -0
  196. roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
  197. roadmap/adapters/persistence/storage/project_storage.py +102 -0
  198. roadmap/adapters/persistence/storage/queries.py +227 -0
  199. roadmap/adapters/persistence/storage/state_manager.py +652 -0
  200. roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
  201. roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
  202. roadmap/adapters/persistence/sync_orchestrator.py +293 -0
  203. roadmap/adapters/persistence/sync_state_tracker.py +86 -0
  204. roadmap/adapters/persistence/yaml_repositories.py +711 -0
  205. roadmap/adapters/sync/__init__.py +21 -0
  206. roadmap/adapters/sync/backend_factory.py +108 -0
  207. roadmap/adapters/sync/backends/__init__.py +15 -0
  208. roadmap/adapters/sync/backends/converters.py +209 -0
  209. roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
  210. roadmap/adapters/sync/backends/github_client.py +99 -0
  211. roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
  212. roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
  213. roadmap/adapters/sync/backends/services/__init__.py +1 -0
  214. roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
  215. roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
  216. roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
  217. roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
  218. roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
  219. roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
  220. roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
  221. roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
  222. roadmap/adapters/sync/services/__init__.py +11 -0
  223. roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
  224. roadmap/adapters/sync/services/conflict_converter.py +85 -0
  225. roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
  226. roadmap/adapters/sync/services/issue_state_service.py +243 -0
  227. roadmap/adapters/sync/services/local_change_filter.py +129 -0
  228. roadmap/adapters/sync/services/pull_result_processor.py +71 -0
  229. roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
  230. roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
  231. roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
  232. roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
  233. roadmap/adapters/sync/services/sync_linking_service.py +329 -0
  234. roadmap/adapters/sync/services/sync_report_service.py +62 -0
  235. roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
  236. roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
  237. roadmap/adapters/sync/sync_merge_engine.py +416 -0
  238. roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
  239. roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
  240. roadmap/application/services/__init__.py +1 -0
  241. roadmap/application/services/deduplicate_service.py +517 -0
  242. roadmap/application/use_cases/__init__.py +1 -0
  243. roadmap/common/__init__.py +308 -0
  244. roadmap/common/cache.py +209 -0
  245. roadmap/common/cli_errors.py +147 -0
  246. roadmap/common/configuration/__init__.py +33 -0
  247. roadmap/common/configuration/config_loader.py +264 -0
  248. roadmap/common/configuration/config_manager.py +164 -0
  249. roadmap/common/configuration/config_schema.py +145 -0
  250. roadmap/common/configuration/github/__init__.py +9 -0
  251. roadmap/common/configuration/github/config_manager.py +154 -0
  252. roadmap/common/configuration/github/token_resolver.py +84 -0
  253. roadmap/common/console.py +184 -0
  254. roadmap/common/constants.py +134 -0
  255. roadmap/common/datetime_parser.py +319 -0
  256. roadmap/common/error_formatter.py +84 -0
  257. roadmap/common/errors/__init__.py +143 -0
  258. roadmap/common/errors/error_base.py +73 -0
  259. roadmap/common/errors/error_file.py +82 -0
  260. roadmap/common/errors/error_git.py +67 -0
  261. roadmap/common/errors/error_handler.py +166 -0
  262. roadmap/common/errors/error_network.py +90 -0
  263. roadmap/common/errors/error_security.py +78 -0
  264. roadmap/common/errors/error_standards.py +561 -0
  265. roadmap/common/errors/error_validation.py +111 -0
  266. roadmap/common/errors/exceptions.py +432 -0
  267. roadmap/common/formatters/__init__.py +29 -0
  268. roadmap/common/formatters/base_table_formatter.py +96 -0
  269. roadmap/common/formatters/export/__init__.py +7 -0
  270. roadmap/common/formatters/export/issue_exporter.py +97 -0
  271. roadmap/common/formatters/helpers.py +28 -0
  272. roadmap/common/formatters/kanban/__init__.py +9 -0
  273. roadmap/common/formatters/kanban/layout.py +48 -0
  274. roadmap/common/formatters/kanban/organizer.py +81 -0
  275. roadmap/common/formatters/output/__init__.py +15 -0
  276. roadmap/common/formatters/tables/__init__.py +11 -0
  277. roadmap/common/formatters/tables/column_factory.py +312 -0
  278. roadmap/common/formatters/tables/issue_table.py +276 -0
  279. roadmap/common/formatters/tables/milestone_table.py +247 -0
  280. roadmap/common/formatters/tables/project_table.py +195 -0
  281. roadmap/common/formatters/text/__init__.py +57 -0
  282. roadmap/common/formatters/text/basic.py +217 -0
  283. roadmap/common/formatters/text/display.py +42 -0
  284. roadmap/common/formatters/text/duration.py +36 -0
  285. roadmap/common/formatters/text/operations.py +325 -0
  286. roadmap/common/formatters/text/status_badges.py +57 -0
  287. roadmap/common/initialization/github/__init__.py +11 -0
  288. roadmap/common/initialization/github/setup_service.py +238 -0
  289. roadmap/common/initialization/github/setup_validator.py +78 -0
  290. roadmap/common/logging/__init__.py +113 -0
  291. roadmap/common/logging/audit_logging.py +5 -0
  292. roadmap/common/logging/correlation.py +45 -0
  293. roadmap/common/logging/decorators.py +267 -0
  294. roadmap/common/logging/error_logging.py +217 -0
  295. roadmap/common/logging/formatters.py +124 -0
  296. roadmap/common/logging/loggers.py +68 -0
  297. roadmap/common/logging/performance_tracking.py +267 -0
  298. roadmap/common/logging/utils.py +335 -0
  299. roadmap/common/models/__init__.py +44 -0
  300. roadmap/common/models/cli_models.py +118 -0
  301. roadmap/common/models/config_models.py +199 -0
  302. roadmap/common/models/output_models.py +381 -0
  303. roadmap/common/observability/__init__.py +28 -0
  304. roadmap/common/observability/instrumentation.py +103 -0
  305. roadmap/common/observability/observability.py +76 -0
  306. roadmap/common/observability/otel_init.py +109 -0
  307. roadmap/common/output_formatter.py +393 -0
  308. roadmap/common/progress.py +379 -0
  309. roadmap/common/result.py +396 -0
  310. roadmap/common/security/__init__.py +48 -0
  311. roadmap/common/security/exceptions.py +13 -0
  312. roadmap/common/security/export_cleanup.py +79 -0
  313. roadmap/common/security/file_operations.py +104 -0
  314. roadmap/common/security/filename_sanitization.py +54 -0
  315. roadmap/common/security/logging.py +27 -0
  316. roadmap/common/security/path_validation.py +137 -0
  317. roadmap/common/security/temp_files.py +41 -0
  318. roadmap/common/services/__init__.py +65 -0
  319. roadmap/common/services/decorators.py +183 -0
  320. roadmap/common/services/logging_utils.py +208 -0
  321. roadmap/common/services/metrics.py +161 -0
  322. roadmap/common/services/performance.py +219 -0
  323. roadmap/common/services/profiling.py +288 -0
  324. roadmap/common/services/retry.py +288 -0
  325. roadmap/common/status_style_manager.py +114 -0
  326. roadmap/common/union_find.py +88 -0
  327. roadmap/common/update_constants.py +13 -0
  328. roadmap/common/utils/__init__.py +65 -0
  329. roadmap/common/utils/cli_helpers.py +381 -0
  330. roadmap/common/utils/file_utils.py +440 -0
  331. roadmap/common/utils/path_utils.py +38 -0
  332. roadmap/common/utils/status_utils.py +130 -0
  333. roadmap/common/utils/timezone_utils.py +480 -0
  334. roadmap/common/validation/__init__.py +37 -0
  335. roadmap/common/validation/field_validator.py +104 -0
  336. roadmap/common/validation/result.py +37 -0
  337. roadmap/common/validation/roadmap_validator.py +310 -0
  338. roadmap/common/validation/schema_validator.py +36 -0
  339. roadmap/common/validation/validators.py +50 -0
  340. roadmap/core/__init__.py +1 -0
  341. roadmap/core/domain/__init__.py +30 -0
  342. roadmap/core/domain/comment.py +24 -0
  343. roadmap/core/domain/health.py +15 -0
  344. roadmap/core/domain/issue.py +274 -0
  345. roadmap/core/domain/milestone.py +173 -0
  346. roadmap/core/domain/ports/__init__.py +1 -0
  347. roadmap/core/domain/ports/baseline_repository.py +24 -0
  348. roadmap/core/domain/ports/issue_repository.py +34 -0
  349. roadmap/core/domain/ports/remote_backend_port.py +31 -0
  350. roadmap/core/domain/project.py +130 -0
  351. roadmap/core/interfaces/__init__.py +109 -0
  352. roadmap/core/interfaces/assignee_validator.py +40 -0
  353. roadmap/core/interfaces/backend_factory.py +175 -0
  354. roadmap/core/interfaces/github.py +102 -0
  355. roadmap/core/interfaces/parsers.py +140 -0
  356. roadmap/core/interfaces/persistence.py +45 -0
  357. roadmap/core/interfaces/repositories.py +174 -0
  358. roadmap/core/interfaces/state_managers.py +111 -0
  359. roadmap/core/interfaces/state_storage.py +64 -0
  360. roadmap/core/interfaces/sync_backend.py +275 -0
  361. roadmap/core/interfaces/sync_services.py +119 -0
  362. roadmap/core/interfaces/sync_validators.py +180 -0
  363. roadmap/core/models/__init__.py +31 -0
  364. roadmap/core/models/sync_models.py +162 -0
  365. roadmap/core/models.py +115 -0
  366. roadmap/core/observability/__init__.py +17 -0
  367. roadmap/core/observability/sync_metrics.py +699 -0
  368. roadmap/core/repositories.py +200 -0
  369. roadmap/core/services/__init__.py +313 -0
  370. roadmap/core/services/baseline/__init__.py +1 -0
  371. roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
  372. roadmap/core/services/baseline/baseline_retriever.py +63 -0
  373. roadmap/core/services/baseline/baseline_selector.py +248 -0
  374. roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
  375. roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
  376. roadmap/core/services/comment/__init__.py +1 -0
  377. roadmap/core/services/comment/comment_service.py +277 -0
  378. roadmap/core/services/git/__init__.py +1 -0
  379. roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
  380. roadmap/core/services/github/__init__.py +1 -0
  381. roadmap/core/services/github/github_change_detector.py +162 -0
  382. roadmap/core/services/github/github_config_validator.py +148 -0
  383. roadmap/core/services/github/github_conflict_detector.py +179 -0
  384. roadmap/core/services/github/github_entity_classifier.py +51 -0
  385. roadmap/core/services/github/github_integration_service.py +439 -0
  386. roadmap/core/services/github/github_issue_client.py +346 -0
  387. roadmap/core/services/health/__init__.py +1 -0
  388. roadmap/core/services/health/backup_cleanup_service.py +202 -0
  389. roadmap/core/services/health/data_integrity_validator_service.py +90 -0
  390. roadmap/core/services/health/entity_health_scanner.py +460 -0
  391. roadmap/core/services/health/file_repair_service.py +193 -0
  392. roadmap/core/services/health/health_check_service.py +166 -0
  393. roadmap/core/services/health/health_models.py +80 -0
  394. roadmap/core/services/health/infrastructure_validator_service.py +328 -0
  395. roadmap/core/services/health/issue_health_scanner.py +263 -0
  396. roadmap/core/services/helpers/__init__.py +18 -0
  397. roadmap/core/services/helpers/status_change_helpers.py +88 -0
  398. roadmap/core/services/initialization/__init__.py +19 -0
  399. roadmap/core/services/initialization/utils.py +104 -0
  400. roadmap/core/services/initialization/validator.py +78 -0
  401. roadmap/core/services/initialization/workflow.py +182 -0
  402. roadmap/core/services/initialization_service.py +111 -0
  403. roadmap/core/services/issue/__init__.py +1 -0
  404. roadmap/core/services/issue/assignee_validation_service.py +312 -0
  405. roadmap/core/services/issue/issue_creation_service.py +309 -0
  406. roadmap/core/services/issue/issue_filter_service.py +302 -0
  407. roadmap/core/services/issue/issue_matching_service.py +127 -0
  408. roadmap/core/services/issue/issue_service.py +765 -0
  409. roadmap/core/services/issue/issue_update_service.py +144 -0
  410. roadmap/core/services/issue/start_issue_service.py +174 -0
  411. roadmap/core/services/issue_helpers/__init__.py +18 -0
  412. roadmap/core/services/issue_helpers/issue_filters.py +296 -0
  413. roadmap/core/services/milestone_service.py +438 -0
  414. roadmap/core/services/project/__init__.py +1 -0
  415. roadmap/core/services/project/project_service.py +434 -0
  416. roadmap/core/services/project/project_status_service.py +125 -0
  417. roadmap/core/services/project_init/__init__.py +22 -0
  418. roadmap/core/services/project_init/context_detection.py +146 -0
  419. roadmap/core/services/project_init/creation.py +76 -0
  420. roadmap/core/services/project_init/detection.py +51 -0
  421. roadmap/core/services/project_init/template.py +228 -0
  422. roadmap/core/services/status_change_service.py +88 -0
  423. roadmap/core/services/sync/__init__.py +1 -0
  424. roadmap/core/services/sync/batch_processor.py +290 -0
  425. roadmap/core/services/sync/conflict_resolver.py +154 -0
  426. roadmap/core/services/sync/dependency_resolver.py +367 -0
  427. roadmap/core/services/sync/duplicate_detector.py +771 -0
  428. roadmap/core/services/sync/duplicate_resolver.py +315 -0
  429. roadmap/core/services/sync/error_classification.py +470 -0
  430. roadmap/core/services/sync/performance.py +263 -0
  431. roadmap/core/services/sync/sync_change_computer.py +211 -0
  432. roadmap/core/services/sync/sync_checkpoint.py +348 -0
  433. roadmap/core/services/sync/sync_conflict_detector.py +147 -0
  434. roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
  435. roadmap/core/services/sync/sync_errors.py +383 -0
  436. roadmap/core/services/sync/sync_key_normalizer.py +222 -0
  437. roadmap/core/services/sync/sync_metadata_service.py +361 -0
  438. roadmap/core/services/sync/sync_plan.py +171 -0
  439. roadmap/core/services/sync/sync_plan_executor.py +447 -0
  440. roadmap/core/services/sync/sync_report.py +296 -0
  441. roadmap/core/services/sync/sync_state.py +173 -0
  442. roadmap/core/services/sync/sync_state_comparator.py +690 -0
  443. roadmap/core/services/sync/sync_state_manager.py +431 -0
  444. roadmap/core/services/sync/sync_state_normalizer.py +98 -0
  445. roadmap/core/services/sync/sync_three_way.py +78 -0
  446. roadmap/core/services/sync/three_way_merger.py +178 -0
  447. roadmap/core/services/utils/__init__.py +1 -0
  448. roadmap/core/services/utils/configuration_service.py +113 -0
  449. roadmap/core/services/utils/critical_path_calculator.py +418 -0
  450. roadmap/core/services/utils/dependency_analyzer.py +360 -0
  451. roadmap/core/services/utils/field_conflict_detector.py +126 -0
  452. roadmap/core/services/utils/remote_fetcher.py +202 -0
  453. roadmap/core/services/utils/remote_state_normalizer.py +110 -0
  454. roadmap/core/services/utils/retry_policy.py +51 -0
  455. roadmap/core/services/validator_base.py +130 -0
  456. roadmap/core/services/validators/__init__.py +28 -0
  457. roadmap/core/services/validators/_utils.py +20 -0
  458. roadmap/core/services/validators/archivable_issues_validator.py +71 -0
  459. roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
  460. roadmap/core/services/validators/backup_validator.py +89 -0
  461. roadmap/core/services/validators/data_integrity_validator.py +84 -0
  462. roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
  463. roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
  464. roadmap/core/services/validators/folder_structure_validator.py +165 -0
  465. roadmap/core/services/validators/health_status_utils.py +28 -0
  466. roadmap/core/services/validators/milestone_naming_validator.py +146 -0
  467. roadmap/core/services/validators/missing_headlines_validator.py +97 -0
  468. roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
  469. roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
  470. roadmap/core/utils/git_remote_parser.py +83 -0
  471. roadmap/core/utils/github_urls.py +66 -0
  472. roadmap/domain/validation.py +30 -0
  473. roadmap/infrastructure/__init__.py +55 -0
  474. roadmap/infrastructure/coordination/__init__.py +8 -0
  475. roadmap/infrastructure/coordination/coordinator_params.py +82 -0
  476. roadmap/infrastructure/coordination/core.py +446 -0
  477. roadmap/infrastructure/coordination/git_coordinator.py +160 -0
  478. roadmap/infrastructure/coordination/initialization.py +348 -0
  479. roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
  480. roadmap/infrastructure/coordination/issue_operations.py +456 -0
  481. roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
  482. roadmap/infrastructure/coordination/milestone_operations.py +201 -0
  483. roadmap/infrastructure/coordination/project_coordinator.py +113 -0
  484. roadmap/infrastructure/coordination/project_operations.py +144 -0
  485. roadmap/infrastructure/coordination/team_coordinator.py +107 -0
  486. roadmap/infrastructure/coordination/user_operations.py +150 -0
  487. roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
  488. roadmap/infrastructure/coordination_gateway.py +152 -0
  489. roadmap/infrastructure/git/__init__.py +6 -0
  490. roadmap/infrastructure/git/git_integration_ops.py +234 -0
  491. roadmap/infrastructure/git_gateway.py +35 -0
  492. roadmap/infrastructure/github_gateway.py +52 -0
  493. roadmap/infrastructure/maintenance/__init__.py +5 -0
  494. roadmap/infrastructure/maintenance/cleanup.py +483 -0
  495. roadmap/infrastructure/observability/__init__.py +7 -0
  496. roadmap/infrastructure/observability/health.py +196 -0
  497. roadmap/infrastructure/observability/health_formatter.py +73 -0
  498. roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
  499. roadmap/infrastructure/persistence_gateway.py +164 -0
  500. roadmap/infrastructure/security/__init__.py +1 -0
  501. roadmap/infrastructure/security/credentials.py +489 -0
  502. roadmap/infrastructure/sync_gateway.py +94 -0
  503. roadmap/infrastructure/validation/__init__.py +7 -0
  504. roadmap/infrastructure/validation/file_enumeration.py +219 -0
  505. roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
  506. roadmap/infrastructure/validation/github_validator.py +101 -0
  507. roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
  508. roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
  509. roadmap/infrastructure/validation_gateway.py +96 -0
  510. roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
  511. roadmap/settings.py +345 -0
  512. roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
  513. roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
  514. roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
  515. roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
  516. roadmap/version.py +381 -0
  517. roadmap_cli-0.1.1.dist-info/METADATA +448 -0
  518. roadmap_cli-0.1.1.dist-info/RECORD +521 -0
  519. roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
  520. roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
  521. roadmap_cli-0.1.1.dist-info/licenses/LICENSE.md +21 -0
@@ -0,0 +1,337 @@
1
+ """Base handler for GitHub API operations."""
2
+
3
+ from typing import Any, TypedDict
4
+
5
+ import requests
6
+
7
+ from roadmap.adapters.base_paginated_adapter import BasePaginatedAdapter
8
+ from roadmap.common.logging import get_logger, log_external_service_error
9
+
10
+
11
+ class GitHubAPIError(Exception):
12
+ """Exception raised for GitHub API errors."""
13
+
14
+ pass
15
+
16
+
17
+ class _ExternalLogKwargs(TypedDict):
18
+ service_name: str
19
+ operation: str
20
+ retry_count: int
21
+
22
+
23
+ class BaseGitHubHandler(BasePaginatedAdapter):
24
+ """Base class for GitHub API handlers.
25
+
26
+ Inherits pagination support from BasePaginatedAdapter for handling
27
+ GitHub's paginated API responses.
28
+ """
29
+
30
+ BASE_URL = "https://api.github.com"
31
+
32
+ def __init__(
33
+ self,
34
+ session: requests.Session,
35
+ owner: str | None = None,
36
+ repo: str | None = None,
37
+ ):
38
+ """Initialize handler with GitHub API session.
39
+
40
+ Args:
41
+ session: Requests session with authentication headers
42
+ owner: Repository owner (username or organization)
43
+ repo: Repository name
44
+ """
45
+ self.session = session
46
+ self.owner = owner
47
+ self.repo = repo
48
+
49
+ def set_repository(self, owner: str, repo: str) -> None:
50
+ """Set the target repository."""
51
+ self.owner = owner
52
+ self.repo = repo
53
+
54
+ def _check_repository(self) -> None:
55
+ """Check if repository is set."""
56
+ if not self.owner or not self.repo:
57
+ raise GitHubAPIError(
58
+ "Repository not set. Use set_repository() or provide owner/repo in constructor."
59
+ )
60
+
61
+ def _raise_http_error(
62
+ self,
63
+ status_code: int,
64
+ response: requests.Response,
65
+ method: str,
66
+ endpoint: str,
67
+ e: requests.exceptions.HTTPError,
68
+ ) -> None:
69
+ """Log and raise a GitHubAPIError for the given HTTP status code."""
70
+ retry_count = getattr(self.session, "retry_count", 0)
71
+ if not isinstance(retry_count, int):
72
+ retry_count = 0
73
+
74
+ log_kwargs: _ExternalLogKwargs = {
75
+ "service_name": "GitHub API",
76
+ "operation": f"{method} {endpoint}",
77
+ "retry_count": retry_count,
78
+ }
79
+
80
+ handlers: dict[int, Any] = {
81
+ 400: self._handle_400,
82
+ 401: self._handle_401,
83
+ 403: self._handle_403,
84
+ 404: self._handle_404,
85
+ 410: self._handle_410,
86
+ 422: self._handle_422,
87
+ 429: self._handle_429,
88
+ }
89
+ handler = handlers.get(status_code)
90
+ if handler:
91
+ handler(status_code, response, method, endpoint, e, log_kwargs)
92
+ return
93
+
94
+ if 500 <= status_code < 600:
95
+ self._handle_5xx(status_code, method, endpoint, e, log_kwargs)
96
+ return
97
+
98
+ self._handle_unknown(status_code, method, endpoint, e, log_kwargs)
99
+
100
+ def _handle_400(
101
+ self,
102
+ status_code: int,
103
+ _response: requests.Response,
104
+ method: str,
105
+ endpoint: str,
106
+ error: requests.exceptions.HTTPError,
107
+ log_kwargs: _ExternalLogKwargs,
108
+ ) -> None:
109
+ log_external_service_error(error=error, **log_kwargs)
110
+ get_logger().warning(
111
+ "github_api_bad_request",
112
+ status_code=status_code,
113
+ operation=f"{method} {endpoint}",
114
+ severity="data_error",
115
+ )
116
+ raise GitHubAPIError("Bad Request: Invalid request payload") from error
117
+
118
+ def _handle_401(
119
+ self,
120
+ status_code: int,
121
+ _response: requests.Response,
122
+ method: str,
123
+ endpoint: str,
124
+ error: requests.exceptions.HTTPError,
125
+ log_kwargs: _ExternalLogKwargs,
126
+ ) -> None:
127
+ _ = method, endpoint
128
+ log_external_service_error(error=error, **log_kwargs)
129
+ get_logger().warning(
130
+ "github_api_authentication_failed",
131
+ status_code=status_code,
132
+ severity="config",
133
+ )
134
+ raise GitHubAPIError(
135
+ "Authentication failed. Check your GitHub token."
136
+ ) from error
137
+
138
+ def _extract_forbidden_response_message(
139
+ self, response: requests.Response
140
+ ) -> str | None:
141
+ """Best-effort parsing of GitHub forbidden response message."""
142
+ try:
143
+ if response.content:
144
+ payload = response.json()
145
+ if isinstance(payload, dict):
146
+ message = payload.get("message")
147
+ return str(message) if message is not None else None
148
+ except Exception as parse_error:
149
+ get_logger().warning(
150
+ "github_api_forbidden_response_parse_failed",
151
+ error=str(parse_error),
152
+ severity="operational",
153
+ )
154
+ return None
155
+
156
+ def _handle_403(
157
+ self,
158
+ status_code: int,
159
+ response: requests.Response,
160
+ method: str,
161
+ endpoint: str,
162
+ error: requests.exceptions.HTTPError,
163
+ _log_kwargs: _ExternalLogKwargs,
164
+ ) -> None:
165
+ response_message = self._extract_forbidden_response_message(response)
166
+ get_logger().warning(
167
+ "github_api_access_forbidden",
168
+ status_code=status_code,
169
+ operation=f"{method} {endpoint}",
170
+ owner=self.owner,
171
+ repo=self.repo,
172
+ rate_limit_remaining=response.headers.get("X-RateLimit-Remaining"),
173
+ rate_limit_reset=response.headers.get("X-RateLimit-Reset"),
174
+ response_message=response_message,
175
+ severity="config",
176
+ )
177
+ raise GitHubAPIError(
178
+ "Access forbidden. Check repository permissions and token scopes."
179
+ ) from error
180
+
181
+ def _handle_404(
182
+ self,
183
+ status_code: int,
184
+ _response: requests.Response,
185
+ method: str,
186
+ endpoint: str,
187
+ error: requests.exceptions.HTTPError,
188
+ log_kwargs: _ExternalLogKwargs,
189
+ ) -> None:
190
+ log_external_service_error(error=error, **log_kwargs)
191
+ get_logger().warning(
192
+ "github_api_resource_not_found",
193
+ status_code=status_code,
194
+ operation=f"{method} {endpoint}",
195
+ severity="operational",
196
+ )
197
+ raise GitHubAPIError("Repository or resource not found.") from error
198
+
199
+ def _handle_410(
200
+ self,
201
+ status_code: int,
202
+ _response: requests.Response,
203
+ method: str,
204
+ endpoint: str,
205
+ error: requests.exceptions.HTTPError,
206
+ _log_kwargs: _ExternalLogKwargs,
207
+ ) -> None:
208
+ get_logger().info(
209
+ "github_api_resource_gone",
210
+ status_code=status_code,
211
+ operation=f"{method} {endpoint}",
212
+ severity="operational",
213
+ )
214
+ raise GitHubAPIError("Resource has been deleted (410 Gone)") from error
215
+
216
+ def _handle_422(
217
+ self,
218
+ status_code: int,
219
+ response: requests.Response,
220
+ method: str,
221
+ endpoint: str,
222
+ error: requests.exceptions.HTTPError,
223
+ _log_kwargs: _ExternalLogKwargs,
224
+ ) -> None:
225
+ _ = method, endpoint
226
+ error_data = response.json() if response.content else {}
227
+ error_details = [
228
+ f"{err.get('field', 'unknown')}:{err.get('code', 'unknown')} {err.get('message', '')}"
229
+ for err in error_data.get("errors", [])
230
+ if isinstance(err, dict)
231
+ ]
232
+ error_msg = error_data.get("message", "Validation failed")
233
+ if error_details:
234
+ error_msg = f"{error_msg} - {'; '.join(error_details)}"
235
+ get_logger().warning(
236
+ "github_api_validation_error",
237
+ status_code=status_code,
238
+ validation_errors=error_details,
239
+ severity="data_error",
240
+ )
241
+ raise GitHubAPIError(f"Validation error: {error_msg}") from error
242
+
243
+ def _handle_429(
244
+ self,
245
+ status_code: int,
246
+ response: requests.Response,
247
+ method: str,
248
+ endpoint: str,
249
+ error: requests.exceptions.HTTPError,
250
+ _log_kwargs: _ExternalLogKwargs,
251
+ ) -> None:
252
+ _ = method, endpoint
253
+ retry_after = response.headers.get("Retry-After", "unknown")
254
+ get_logger().warning(
255
+ "github_api_rate_limited",
256
+ status_code=status_code,
257
+ retry_after=retry_after,
258
+ severity="operational",
259
+ )
260
+ raise GitHubAPIError("Rate limit exceeded. Please try again later.") from error
261
+
262
+ def _handle_5xx(
263
+ self,
264
+ status_code: int,
265
+ method: str,
266
+ endpoint: str,
267
+ error: requests.exceptions.HTTPError,
268
+ log_kwargs: _ExternalLogKwargs,
269
+ ) -> None:
270
+ log_external_service_error(error=error, **log_kwargs)
271
+ get_logger().warning(
272
+ "github_api_server_error",
273
+ status_code=status_code,
274
+ operation=f"{method} {endpoint}",
275
+ severity="infrastructure",
276
+ )
277
+ raise GitHubAPIError(f"GitHub API server error ({status_code})") from error
278
+
279
+ def _handle_unknown(
280
+ self,
281
+ status_code: int,
282
+ method: str,
283
+ endpoint: str,
284
+ error: requests.exceptions.HTTPError,
285
+ log_kwargs: _ExternalLogKwargs,
286
+ ) -> None:
287
+ log_external_service_error(error=error, **log_kwargs)
288
+ get_logger().warning(
289
+ "github_api_unknown_error",
290
+ status_code=status_code,
291
+ operation=f"{method} {endpoint}",
292
+ severity="operational",
293
+ )
294
+ raise GitHubAPIError(f"GitHub API error ({status_code}): {error}") from error
295
+
296
+ def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
297
+ """Make a request to the GitHub API.
298
+
299
+ Args:
300
+ method: HTTP method (GET, POST, etc.)
301
+ endpoint: API endpoint path
302
+ **kwargs: Additional arguments to pass to requests
303
+
304
+ Returns:
305
+ Response object
306
+
307
+ Raises:
308
+ GitHubAPIError: If the request fails
309
+ """
310
+ url = f"{self.BASE_URL}{endpoint}"
311
+
312
+ try:
313
+ response = self.session.request(method, url, **kwargs)
314
+ response.raise_for_status()
315
+ return response
316
+ except requests.exceptions.HTTPError as e:
317
+ self._raise_http_error(response.status_code, response, method, endpoint, e)
318
+ raise # unreachable but satisfies type checkers
319
+ except requests.exceptions.RequestException as e:
320
+ log_external_service_error(
321
+ error=e,
322
+ service_name="GitHub API",
323
+ operation=f"{method} {endpoint}",
324
+ retry_count=0,
325
+ )
326
+ raise GitHubAPIError(f"Request failed: {e}") from e
327
+
328
+ def test_authentication(self) -> dict[str, Any]:
329
+ """Test authentication and get user info."""
330
+ response = self._make_request("GET", "/user")
331
+ return response.json()
332
+
333
+ def test_repository_access(self) -> dict[str, Any]:
334
+ """Test repository access."""
335
+ self._check_repository()
336
+ response = self._make_request("GET", f"/repos/{self.owner}/{self.repo}")
337
+ return response.json()
@@ -0,0 +1,165 @@
1
+ """GitHub Collaborators handler."""
2
+
3
+ from structlog import get_logger
4
+
5
+ from roadmap.adapters.github.handlers.base import BaseGitHubHandler
6
+
7
+ logger = get_logger()
8
+
9
+
10
+ class CollaboratorsHandler(BaseGitHubHandler):
11
+ """Handler for GitHub team and collaborators API operations."""
12
+
13
+ def get_current_user(self) -> str:
14
+ """Get the current authenticated user's login."""
15
+ response = self._make_request("GET", "/user")
16
+ return response.json()["login"]
17
+
18
+ def get_repository_collaborators(self) -> list[str]:
19
+ """Get all collaborators for the repository.
20
+
21
+ Returns:
22
+ List of usernames of repository collaborators
23
+ """
24
+ self._check_repository()
25
+
26
+ # Get direct collaborators
27
+ collaborators = []
28
+ page = 1
29
+ per_page = 100
30
+
31
+ while True:
32
+ response = self._make_request(
33
+ "GET",
34
+ f"/repos/{self.owner}/{self.repo}/collaborators",
35
+ params={"page": page, "per_page": per_page},
36
+ )
37
+
38
+ page_collaborators = response.json()
39
+ if not page_collaborators:
40
+ break
41
+
42
+ collaborators.extend([collab["login"] for collab in page_collaborators])
43
+
44
+ if len(page_collaborators) < per_page:
45
+ break
46
+
47
+ page += 1
48
+
49
+ return sorted(collaborators)
50
+
51
+ def get_repository_contributors(self) -> list[str]:
52
+ """Get all contributors to the repository.
53
+
54
+ Returns:
55
+ List of usernames of repository contributors
56
+ """
57
+ self._check_repository()
58
+
59
+ contributors = []
60
+ page = 1
61
+ per_page = 100
62
+
63
+ while True:
64
+ response = self._make_request(
65
+ "GET",
66
+ f"/repos/{self.owner}/{self.repo}/contributors",
67
+ params={"page": page, "per_page": per_page},
68
+ )
69
+
70
+ page_contributors = response.json()
71
+ if not page_contributors:
72
+ break
73
+
74
+ contributors.extend([contrib["login"] for contrib in page_contributors])
75
+
76
+ if len(page_contributors) < per_page:
77
+ break
78
+
79
+ page += 1
80
+
81
+ return sorted(contributors)
82
+
83
+ def get_team_members(self) -> list[str]:
84
+ """Get all team members (collaborators and contributors combined).
85
+
86
+ Returns:
87
+ List of unique usernames from collaborators and contributors
88
+ """
89
+ collaborators = self.get_repository_collaborators()
90
+ contributors = self.get_repository_contributors()
91
+
92
+ # Combine and deduplicate
93
+ team_members = list(set(collaborators + contributors))
94
+ return sorted(team_members)
95
+
96
+ def validate_user_exists(self, username: str) -> bool:
97
+ """Check if a GitHub user exists.
98
+
99
+ Args:
100
+ username: GitHub username to validate
101
+
102
+ Returns:
103
+ True if user exists, False otherwise
104
+ """
105
+ try:
106
+ response = self._make_request("GET", f"/users/{username}")
107
+ return response.status_code == 200
108
+ except Exception as e:
109
+ logger.error(
110
+ "user_validation_failed",
111
+ username=username,
112
+ error=str(e),
113
+ severity="infrastructure",
114
+ )
115
+ return False
116
+
117
+ def validate_user_has_repository_access(self, username: str) -> bool:
118
+ """Check if a user has access to the repository (is collaborator or contributor).
119
+
120
+ Args:
121
+ username: GitHub username to validate
122
+
123
+ Returns:
124
+ True if user has repository access, False otherwise
125
+ """
126
+ try:
127
+ team_members = self.get_team_members()
128
+ return username in team_members
129
+ except Exception as e:
130
+ logger.error(
131
+ "access_validation_failed",
132
+ username=username,
133
+ error=str(e),
134
+ severity="infrastructure",
135
+ )
136
+ return False
137
+
138
+ def validate_assignee(self, assignee: str) -> tuple[bool, str]:
139
+ """Validate an assignee for issue assignment.
140
+
141
+ Args:
142
+ assignee: Username to validate
143
+
144
+ Returns:
145
+ Tuple of (is_valid, error_message)
146
+ - (True, "") if valid
147
+ - (False, error_message) if invalid
148
+ """
149
+ if not assignee or not assignee.strip():
150
+ return False, "Assignee cannot be empty"
151
+
152
+ assignee = assignee.strip()
153
+
154
+ # First check if user exists on GitHub
155
+ if not self.validate_user_exists(assignee):
156
+ return False, f"GitHub user '{assignee}' does not exist"
157
+
158
+ # Then check if they have repository access
159
+ if not self.validate_user_has_repository_access(assignee):
160
+ return (
161
+ False,
162
+ f"User '{assignee}' does not have access to repository {self.owner}/{self.repo}",
163
+ )
164
+
165
+ return True, ""
@@ -0,0 +1,118 @@
1
+ """GitHub Comments handler."""
2
+
3
+ from roadmap.adapters.github.handlers.base import BaseGitHubHandler
4
+ from roadmap.common.datetime_parser import parse_github_datetime
5
+ from roadmap.core.domain import Comment
6
+
7
+
8
+ class CommentsHandler(BaseGitHubHandler):
9
+ """Handler for GitHub Comments API operations."""
10
+
11
+ def _create_comment_from_response(
12
+ self, comment_data: dict, issue_id: str = ""
13
+ ) -> Comment:
14
+ """Create a Comment object from GitHub API response data.
15
+
16
+ Args:
17
+ comment_data: GitHub API comment response
18
+ issue_id: Issue ID (may be empty for standalone comment endpoints)
19
+
20
+ Returns:
21
+ Comment object
22
+ """
23
+ return Comment(
24
+ id=comment_data["id"],
25
+ issue_id=issue_id,
26
+ author=comment_data["user"]["login"],
27
+ body=comment_data["body"],
28
+ created_at=parse_github_datetime(comment_data["created_at"]),
29
+ updated_at=parse_github_datetime(comment_data["updated_at"]),
30
+ github_url=comment_data["html_url"],
31
+ )
32
+
33
+ def get_issue_comments(self, issue_number: int) -> list[Comment]:
34
+ """Get all comments for a specific issue.
35
+
36
+ Handles pagination automatically - returns all comments.
37
+
38
+ Args:
39
+ issue_number: GitHub issue number
40
+
41
+ Returns:
42
+ List of all Comment objects for the issue
43
+ """
44
+ self._check_repository()
45
+
46
+ comments_data = self._paginate_request(
47
+ "GET", f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments"
48
+ )
49
+
50
+ comments = []
51
+ for comment_data in comments_data:
52
+ comment = self._create_comment_from_response(
53
+ comment_data, issue_id=str(issue_number)
54
+ )
55
+ comments.append(comment)
56
+
57
+ return comments
58
+
59
+ def create_issue_comment(self, issue_number: int, body: str) -> Comment:
60
+ """Create a new comment on an issue.
61
+
62
+ Args:
63
+ issue_number: GitHub issue number
64
+ body: Comment content (markdown)
65
+
66
+ Returns:
67
+ Created Comment object
68
+ """
69
+ self._check_repository()
70
+
71
+ data = {"body": body}
72
+
73
+ response = self._make_request(
74
+ "POST",
75
+ f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
76
+ json=data,
77
+ )
78
+
79
+ comment_data = response.json()
80
+ return self._create_comment_from_response(
81
+ comment_data, issue_id=str(issue_number)
82
+ )
83
+
84
+ def update_issue_comment(self, comment_id: int, body: str) -> Comment:
85
+ """Update an existing comment.
86
+
87
+ Args:
88
+ comment_id: GitHub comment ID
89
+ body: Updated comment content (markdown)
90
+
91
+ Returns:
92
+ Updated Comment object
93
+ """
94
+ self._check_repository()
95
+
96
+ data = {"body": body}
97
+
98
+ response = self._make_request(
99
+ "PATCH",
100
+ f"/repos/{self.owner}/{self.repo}/issues/comments/{comment_id}",
101
+ json=data,
102
+ )
103
+
104
+ comment_data = response.json()
105
+ # We don't get issue number from this endpoint
106
+ return self._create_comment_from_response(comment_data, issue_id="")
107
+
108
+ def delete_issue_comment(self, comment_id: int) -> None:
109
+ """Delete a comment.
110
+
111
+ Args:
112
+ comment_id: GitHub comment ID
113
+ """
114
+ self._check_repository()
115
+
116
+ self._make_request(
117
+ "DELETE", f"/repos/{self.owner}/{self.repo}/issues/comments/{comment_id}"
118
+ )