wxcli 1.2.0__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 (413) hide show
  1. wxcli/__init__.py +4 -0
  2. wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
  3. wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
  4. wxcli/_playbook/.claude/rules/cleanup.md +29 -0
  5. wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
  6. wxcli/_playbook/.claude/rules/org-health.md +14 -0
  7. wxcli/_playbook/.claude/settings.json +8 -0
  8. wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
  9. wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
  10. wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
  11. wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
  12. wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
  13. wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
  14. wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
  15. wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
  16. wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
  17. wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
  18. wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
  19. wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
  20. wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
  21. wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
  22. wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
  23. wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
  24. wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
  25. wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
  26. wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
  27. wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
  28. wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
  29. wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
  30. wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
  31. wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
  32. wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
  33. wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
  34. wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
  35. wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
  36. wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
  37. wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
  38. wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
  39. wxcli/_playbook/CLAUDE.md +373 -0
  40. wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
  41. wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
  42. wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
  43. wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
  44. wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
  45. wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
  46. wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
  47. wxcli/_playbook/docs/reference/admin-partner.md +357 -0
  48. wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
  49. wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
  50. wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
  51. wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
  52. wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
  53. wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
  54. wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
  55. wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
  56. wxcli/_playbook/docs/reference/authentication.md +965 -0
  57. wxcli/_playbook/docs/reference/call-control.md +1205 -0
  58. wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
  59. wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
  60. wxcli/_playbook/docs/reference/call-routing.md +2304 -0
  61. wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
  62. wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
  63. wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
  64. wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
  65. wxcli/_playbook/docs/reference/devices-core.md +1869 -0
  66. wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
  67. wxcli/_playbook/docs/reference/devices-platform.md +467 -0
  68. wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
  69. wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
  70. wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
  71. wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
  72. wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
  73. wxcli/_playbook/docs/reference/meetings-content.md +473 -0
  74. wxcli/_playbook/docs/reference/meetings-core.md +903 -0
  75. wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
  76. wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
  77. wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
  78. wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
  79. wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
  80. wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
  81. wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
  82. wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
  83. wxcli/_playbook/docs/reference/provisioning.md +1366 -0
  84. wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
  85. wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
  86. wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
  87. wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
  88. wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
  89. wxcli/_version.py +24 -0
  90. wxcli/auth.py +137 -0
  91. wxcli/commands/__init__.py +0 -0
  92. wxcli/commands/_registry.py +181 -0
  93. wxcli/commands/activation_email.py +103 -0
  94. wxcli/commands/admin_recordings.py +585 -0
  95. wxcli/commands/analytics.py +105 -0
  96. wxcli/commands/announcement_playlists.py +219 -0
  97. wxcli/commands/announcements.py +519 -0
  98. wxcli/commands/archive_users.py +36 -0
  99. wxcli/commands/attachment_actions.py +60 -0
  100. wxcli/commands/audit_events.py +83 -0
  101. wxcli/commands/authorizations.py +113 -0
  102. wxcli/commands/auto_attendant.py +642 -0
  103. wxcli/commands/broadworks_billing_reports.py +126 -0
  104. wxcli/commands/broadworks_enterprises.py +160 -0
  105. wxcli/commands/broadworks_subscribers.py +319 -0
  106. wxcli/commands/broadworks_workspaces.py +108 -0
  107. wxcli/commands/call_controls.py +1304 -0
  108. wxcli/commands/call_park.py +494 -0
  109. wxcli/commands/call_pickup.py +229 -0
  110. wxcli/commands/call_queue.py +1878 -0
  111. wxcli/commands/call_recording.py +707 -0
  112. wxcli/commands/call_routing.py +1610 -0
  113. wxcli/commands/call_settings_for_me_phase_5.py +212 -0
  114. wxcli/commands/caller_reputation.py +169 -0
  115. wxcli/commands/calling_service.py +305 -0
  116. wxcli/commands/cc_address_book.py +770 -0
  117. wxcli/commands/cc_agent_greetings.py +242 -0
  118. wxcli/commands/cc_agent_summaries.py +39 -0
  119. wxcli/commands/cc_agent_wellbeing.py +216 -0
  120. wxcli/commands/cc_agents.py +425 -0
  121. wxcli/commands/cc_ai_assistant.py +55 -0
  122. wxcli/commands/cc_ai_feature.py +294 -0
  123. wxcli/commands/cc_audio_files.py +149 -0
  124. wxcli/commands/cc_auto_csat.py +311 -0
  125. wxcli/commands/cc_aux_code.py +458 -0
  126. wxcli/commands/cc_business_hour.py +353 -0
  127. wxcli/commands/cc_call_monitoring.py +228 -0
  128. wxcli/commands/cc_callbacks.py +194 -0
  129. wxcli/commands/cc_campaign.py +212 -0
  130. wxcli/commands/cc_captures.py +39 -0
  131. wxcli/commands/cc_contact_list.py +169 -0
  132. wxcli/commands/cc_contact_number.py +295 -0
  133. wxcli/commands/cc_data_sources.py +235 -0
  134. wxcli/commands/cc_desktop_layout.py +380 -0
  135. wxcli/commands/cc_desktop_profile.py +322 -0
  136. wxcli/commands/cc_dial_number.py +511 -0
  137. wxcli/commands/cc_dial_plan.py +347 -0
  138. wxcli/commands/cc_dnc.py +101 -0
  139. wxcli/commands/cc_entry_point.py +545 -0
  140. wxcli/commands/cc_ewt.py +51 -0
  141. wxcli/commands/cc_flow.py +477 -0
  142. wxcli/commands/cc_global_vars.py +430 -0
  143. wxcli/commands/cc_holiday_list.py +335 -0
  144. wxcli/commands/cc_journey.py +1261 -0
  145. wxcli/commands/cc_legacy_flows.py +49 -0
  146. wxcli/commands/cc_multimedia_profile.py +436 -0
  147. wxcli/commands/cc_notification.py +51 -0
  148. wxcli/commands/cc_outdial_ani.py +604 -0
  149. wxcli/commands/cc_overrides.py +341 -0
  150. wxcli/commands/cc_queue.py +1083 -0
  151. wxcli/commands/cc_queue_stats.py +55 -0
  152. wxcli/commands/cc_realtime.py +51 -0
  153. wxcli/commands/cc_resource_collection.py +312 -0
  154. wxcli/commands/cc_search.py +46 -0
  155. wxcli/commands/cc_site.py +400 -0
  156. wxcli/commands/cc_skill.py +350 -0
  157. wxcli/commands/cc_skill_profile.py +289 -0
  158. wxcli/commands/cc_subscriptions.py +426 -0
  159. wxcli/commands/cc_summaries.py +130 -0
  160. wxcli/commands/cc_tasks.py +844 -0
  161. wxcli/commands/cc_team.py +325 -0
  162. wxcli/commands/cc_user_profiles.py +349 -0
  163. wxcli/commands/cc_users.py +701 -0
  164. wxcli/commands/cc_work_types.py +363 -0
  165. wxcli/commands/cdr.py +89 -0
  166. wxcli/commands/classifications.py +38 -0
  167. wxcli/commands/cleanup.py +1427 -0
  168. wxcli/commands/client_settings.py +72 -0
  169. wxcli/commands/conference.py +328 -0
  170. wxcli/commands/configure.py +72 -0
  171. wxcli/commands/converged_recordings.py +347 -0
  172. wxcli/commands/converged_recordings_export.py +310 -0
  173. wxcli/commands/cq_playlists.py +42 -0
  174. wxcli/commands/cucm.py +2794 -0
  175. wxcli/commands/cucm_config.py +129 -0
  176. wxcli/commands/customer_assist.py +373 -0
  177. wxcli/commands/data_sources.py +227 -0
  178. wxcli/commands/dect_devices.py +787 -0
  179. wxcli/commands/device_configurations.py +77 -0
  180. wxcli/commands/device_dynamic_settings.py +320 -0
  181. wxcli/commands/device_settings.py +1489 -0
  182. wxcli/commands/devices.py +280 -0
  183. wxcli/commands/domains.py +142 -0
  184. wxcli/commands/ecm.py +178 -0
  185. wxcli/commands/emergency_services.py +827 -0
  186. wxcli/commands/events.py +83 -0
  187. wxcli/commands/external_voicemail.py +52 -0
  188. wxcli/commands/groups.py +206 -0
  189. wxcli/commands/guest_management.py +76 -0
  190. wxcli/commands/hds.py +340 -0
  191. wxcli/commands/hot_desk.py +67 -0
  192. wxcli/commands/hot_desking_members.py +118 -0
  193. wxcli/commands/hot_desking_portal.py +124 -0
  194. wxcli/commands/hunt_group.py +577 -0
  195. wxcli/commands/hybrid_clusters.py +71 -0
  196. wxcli/commands/hybrid_connectors.py +67 -0
  197. wxcli/commands/identity_org.py +159 -0
  198. wxcli/commands/init_playbook.py +155 -0
  199. wxcli/commands/licenses.py +106 -0
  200. wxcli/commands/live_monitoring.py +40 -0
  201. wxcli/commands/location_call_handling.py +577 -0
  202. wxcli/commands/location_schedules.py +341 -0
  203. wxcli/commands/location_settings.py +1297 -0
  204. wxcli/commands/location_voicemail.py +494 -0
  205. wxcli/commands/locations.py +336 -0
  206. wxcli/commands/meeting_captions.py +110 -0
  207. wxcli/commands/meeting_chats.py +63 -0
  208. wxcli/commands/meeting_invitees.py +234 -0
  209. wxcli/commands/meeting_messages.py +28 -0
  210. wxcli/commands/meeting_participants.py +278 -0
  211. wxcli/commands/meeting_polls.py +111 -0
  212. wxcli/commands/meeting_preferences.py +515 -0
  213. wxcli/commands/meeting_qa.py +78 -0
  214. wxcli/commands/meeting_qualities.py +43 -0
  215. wxcli/commands/meeting_reports.py +101 -0
  216. wxcli/commands/meeting_session_types.py +105 -0
  217. wxcli/commands/meeting_site.py +57 -0
  218. wxcli/commands/meeting_slido.py +44 -0
  219. wxcli/commands/meeting_summaries.py +92 -0
  220. wxcli/commands/meeting_tracking_codes.py +233 -0
  221. wxcli/commands/meeting_transcripts.py +230 -0
  222. wxcli/commands/meetings.py +1997 -0
  223. wxcli/commands/memberships.py +164 -0
  224. wxcli/commands/messages.py +213 -0
  225. wxcli/commands/mode_management.py +244 -0
  226. wxcli/commands/my_call_settings.py +3577 -0
  227. wxcli/commands/numbers.py +405 -0
  228. wxcli/commands/operating_modes.py +410 -0
  229. wxcli/commands/org_contacts.py +284 -0
  230. wxcli/commands/org_health_cli.py +55 -0
  231. wxcli/commands/org_settings.py +71 -0
  232. wxcli/commands/organizations.py +83 -0
  233. wxcli/commands/paging_group.py +257 -0
  234. wxcli/commands/partner_admins.py +141 -0
  235. wxcli/commands/partner_reports.py +188 -0
  236. wxcli/commands/partner_tags.py +216 -0
  237. wxcli/commands/people.py +279 -0
  238. wxcli/commands/person_call_settings.py +71 -0
  239. wxcli/commands/pstn.py +280 -0
  240. wxcli/commands/recording_report.py +154 -0
  241. wxcli/commands/report_templates.py +38 -0
  242. wxcli/commands/reports.py +144 -0
  243. wxcli/commands/resource_group_memberships.py +160 -0
  244. wxcli/commands/resource_groups.py +67 -0
  245. wxcli/commands/roles.py +63 -0
  246. wxcli/commands/room_tabs.py +160 -0
  247. wxcli/commands/rooms.py +225 -0
  248. wxcli/commands/scim_bulk.py +46 -0
  249. wxcli/commands/scim_groups.py +246 -0
  250. wxcli/commands/scim_schemas.py +82 -0
  251. wxcli/commands/scim_users.py +283 -0
  252. wxcli/commands/security_audit.py +54 -0
  253. wxcli/commands/service_apps.py +51 -0
  254. wxcli/commands/single_number_reach.py +209 -0
  255. wxcli/commands/team_memberships.py +155 -0
  256. wxcli/commands/teams.py +153 -0
  257. wxcli/commands/ucm_profile.py +42 -0
  258. wxcli/commands/update.py +219 -0
  259. wxcli/commands/user_settings.py +5116 -0
  260. wxcli/commands/video_mesh.py +1090 -0
  261. wxcli/commands/virtual_extensions.py +502 -0
  262. wxcli/commands/virtual_line_settings.py +2072 -0
  263. wxcli/commands/webhooks.py +176 -0
  264. wxcli/commands/wholesale_billing_reports.py +147 -0
  265. wxcli/commands/wholesale_provisioning.py +523 -0
  266. wxcli/commands/workspace_locations.py +339 -0
  267. wxcli/commands/workspace_metrics.py +103 -0
  268. wxcli/commands/workspace_personalization.py +70 -0
  269. wxcli/commands/workspace_settings.py +3347 -0
  270. wxcli/commands/workspaces.py +279 -0
  271. wxcli/commands/xapi.py +114 -0
  272. wxcli/config.py +139 -0
  273. wxcli/errors.py +77 -0
  274. wxcli/main.py +210 -0
  275. wxcli/migration/CLAUDE.md +106 -0
  276. wxcli/migration/__init__.py +1 -0
  277. wxcli/migration/advisory/CLAUDE.md +196 -0
  278. wxcli/migration/advisory/__init__.py +46 -0
  279. wxcli/migration/advisory/advisor.py +107 -0
  280. wxcli/migration/advisory/advisory_patterns.py +2552 -0
  281. wxcli/migration/advisory/recommendation_rules.py +835 -0
  282. wxcli/migration/cucm/CLAUDE.md +74 -0
  283. wxcli/migration/cucm/__init__.py +5 -0
  284. wxcli/migration/cucm/connection.py +265 -0
  285. wxcli/migration/cucm/discovery.py +225 -0
  286. wxcli/migration/cucm/extractors/__init__.py +1 -0
  287. wxcli/migration/cucm/extractors/announcements.py +92 -0
  288. wxcli/migration/cucm/extractors/base.py +90 -0
  289. wxcli/migration/cucm/extractors/device_profiles.py +98 -0
  290. wxcli/migration/cucm/extractors/devices.py +193 -0
  291. wxcli/migration/cucm/extractors/e911.py +103 -0
  292. wxcli/migration/cucm/extractors/features.py +309 -0
  293. wxcli/migration/cucm/extractors/helpers.py +73 -0
  294. wxcli/migration/cucm/extractors/informational.py +295 -0
  295. wxcli/migration/cucm/extractors/locations.py +221 -0
  296. wxcli/migration/cucm/extractors/moh.py +100 -0
  297. wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
  298. wxcli/migration/cucm/extractors/routing.py +493 -0
  299. wxcli/migration/cucm/extractors/shared_lines.py +146 -0
  300. wxcli/migration/cucm/extractors/templates.py +229 -0
  301. wxcli/migration/cucm/extractors/tier4.py +173 -0
  302. wxcli/migration/cucm/extractors/users.py +240 -0
  303. wxcli/migration/cucm/extractors/voicemail.py +243 -0
  304. wxcli/migration/cucm/extractors/workspaces.py +88 -0
  305. wxcli/migration/cucm/unity_connection.py +304 -0
  306. wxcli/migration/execute/CLAUDE.md +372 -0
  307. wxcli/migration/execute/__init__.py +303 -0
  308. wxcli/migration/execute/batch.py +277 -0
  309. wxcli/migration/execute/dependency.py +483 -0
  310. wxcli/migration/execute/engine.py +964 -0
  311. wxcli/migration/execute/handlers.py +2232 -0
  312. wxcli/migration/execute/planner.py +1939 -0
  313. wxcli/migration/execute/runtime.py +571 -0
  314. wxcli/migration/export/CLAUDE.md +49 -0
  315. wxcli/migration/export/__init__.py +8 -0
  316. wxcli/migration/export/csv_export.py +126 -0
  317. wxcli/migration/export/deployment_plan.py +479 -0
  318. wxcli/migration/export/json_export.py +60 -0
  319. wxcli/migration/models.py +941 -0
  320. wxcli/migration/phone_models.py +50 -0
  321. wxcli/migration/preflight/CLAUDE.md +52 -0
  322. wxcli/migration/preflight/__init__.py +89 -0
  323. wxcli/migration/preflight/checks.py +911 -0
  324. wxcli/migration/preflight/runner.py +293 -0
  325. wxcli/migration/rate_limiter.py +144 -0
  326. wxcli/migration/report/CLAUDE.md +245 -0
  327. wxcli/migration/report/__init__.py +7 -0
  328. wxcli/migration/report/appendix.py +2437 -0
  329. wxcli/migration/report/assembler.py +183 -0
  330. wxcli/migration/report/charts.py +263 -0
  331. wxcli/migration/report/executive.py +776 -0
  332. wxcli/migration/report/explainer.py +704 -0
  333. wxcli/migration/report/helpers.py +74 -0
  334. wxcli/migration/report/ingest.py +152 -0
  335. wxcli/migration/report/notice_templates.py +192 -0
  336. wxcli/migration/report/score.py +375 -0
  337. wxcli/migration/report/styles.py +1052 -0
  338. wxcli/migration/report/user_diff.py +1052 -0
  339. wxcli/migration/report/user_notice.py +425 -0
  340. wxcli/migration/state.py +139 -0
  341. wxcli/migration/store.py +860 -0
  342. wxcli/migration/transform/CLAUDE.md +107 -0
  343. wxcli/migration/transform/__init__.py +12 -0
  344. wxcli/migration/transform/analysis_pipeline.py +453 -0
  345. wxcli/migration/transform/analyzers/__init__.py +142 -0
  346. wxcli/migration/transform/analyzers/css_permission.py +189 -0
  347. wxcli/migration/transform/analyzers/css_routing.py +335 -0
  348. wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
  349. wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
  350. wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
  351. wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
  352. wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
  353. wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
  354. wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
  355. wxcli/migration/transform/analyzers/missing_data.py +300 -0
  356. wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
  357. wxcli/migration/transform/analyzers/shared_line.py +170 -0
  358. wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
  359. wxcli/migration/transform/analyzers/workspace_license.py +212 -0
  360. wxcli/migration/transform/cross_reference.py +1136 -0
  361. wxcli/migration/transform/cucm_pattern.py +246 -0
  362. wxcli/migration/transform/decisions.py +301 -0
  363. wxcli/migration/transform/e164.py +145 -0
  364. wxcli/migration/transform/engine.py +256 -0
  365. wxcli/migration/transform/mappers/CLAUDE.md +123 -0
  366. wxcli/migration/transform/mappers/__init__.py +76 -0
  367. wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
  368. wxcli/migration/transform/mappers/base.py +190 -0
  369. wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
  370. wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
  371. wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
  372. wxcli/migration/transform/mappers/css_mapper.py +868 -0
  373. wxcli/migration/transform/mappers/dect_mapper.py +367 -0
  374. wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
  375. wxcli/migration/transform/mappers/device_mapper.py +227 -0
  376. wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
  377. wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
  378. wxcli/migration/transform/mappers/e911_mapper.py +129 -0
  379. wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
  380. wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
  381. wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
  382. wxcli/migration/transform/mappers/line_mapper.py +310 -0
  383. wxcli/migration/transform/mappers/location_mapper.py +235 -0
  384. wxcli/migration/transform/mappers/moh_mapper.py +114 -0
  385. wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
  386. wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
  387. wxcli/migration/transform/mappers/routing_mapper.py +821 -0
  388. wxcli/migration/transform/mappers/snr_mapper.py +201 -0
  389. wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
  390. wxcli/migration/transform/mappers/user_mapper.py +264 -0
  391. wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
  392. wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
  393. wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
  394. wxcli/migration/transform/normalizers.py +2008 -0
  395. wxcli/migration/transform/pattern_converter.py +106 -0
  396. wxcli/migration/transform/pipeline.py +271 -0
  397. wxcli/migration/transform/rules.py +255 -0
  398. wxcli/org_health/CLAUDE.md +63 -0
  399. wxcli/org_health/__init__.py +7 -0
  400. wxcli/org_health/analyze.py +93 -0
  401. wxcli/org_health/checks.py +458 -0
  402. wxcli/org_health/collector.py +62 -0
  403. wxcli/org_health/models.py +78 -0
  404. wxcli/org_health/report.py +300 -0
  405. wxcli/output.py +101 -0
  406. wxcli-1.2.0.dist-info/METADATA +505 -0
  407. wxcli-1.2.0.dist-info/RECORD +413 -0
  408. wxcli-1.2.0.dist-info/WHEEL +5 -0
  409. wxcli-1.2.0.dist-info/entry_points.txt +2 -0
  410. wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
  411. wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
  412. wxcli-1.2.0.dist-info/scm_version.json +8 -0
  413. wxcli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,303 @@
1
+ """Execution planning layer — models, constants, and tier assignments.
2
+
3
+ Defines the data structures for the execution DAG: operations (nodes),
4
+ dependency types (edge labels), broken cycles, and batches. Also contains
5
+ the static tier assignments and API call estimates sourced from
6
+ 05-dependency-graph.md.
7
+
8
+ (from 05-dependency-graph.md — MigrationOp, DependencyType, batch model)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from enum import Enum
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Enums
20
+ # ---------------------------------------------------------------------------
21
+
22
+ class DependencyType(str, Enum):
23
+ """Edge types in the dependency DAG.
24
+ (from 05-dependency-graph.md lines 104-108)
25
+ """
26
+ REQUIRES = "requires" # hard: A must exist before B can be created
27
+ CONFIGURES = "configures" # hard: A must be created before settings applied
28
+ SOFT = "soft" # breakable: nice-to-have ordering
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Models
33
+ # ---------------------------------------------------------------------------
34
+
35
+ class MigrationOp(BaseModel):
36
+ """One node in the dependency DAG.
37
+ (from 05-dependency-graph.md lines 22-32)
38
+ """
39
+ canonical_id: str # links back to objects table
40
+ op_type: str # "create", "configure", "assign", etc.
41
+ resource_type: str # "location", "user", "device", etc.
42
+ tier: int # from TIER_ASSIGNMENTS
43
+ batch: str | None = None # site/batch assignment
44
+ api_calls: int = 1 # estimated API call count for rate limit budgeting
45
+ description: str = "" # human-readable: "Create user jsmith@acme.com"
46
+ depends_on: list[str] = Field(default_factory=list) # node_ids this op must wait for
47
+ payload: dict | None = None # Bulk ops: handler data that doesn't come from a canonical object
48
+
49
+
50
+ class BrokenCycle(BaseModel):
51
+ """Record of a cycle that was broken during DAG construction.
52
+ (from 05-dependency-graph.md lines 186-193)
53
+ """
54
+ from_node: str
55
+ to_node: str
56
+ dep_type: str | None = None
57
+ reason: str = ""
58
+
59
+
60
+ class Batch(BaseModel):
61
+ """A group of operations to execute together.
62
+ (from 05-dependency-graph.md lines 244-258)
63
+ """
64
+ site: str # "org-wide" or a location canonical_id
65
+ tier: int
66
+ operations: list[str] = Field(default_factory=list) # node_ids
67
+ estimated_api_calls: int = 0
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Static tier assignments
72
+ # (from 05-dependency-graph.md — tier system lines 79-100, batch ordering
73
+ # lines 206-216, expand_to_operations example lines 41-74)
74
+ # ---------------------------------------------------------------------------
75
+
76
+ TIER_ASSIGNMENTS: dict[tuple[str, str], int] = {
77
+ # Tier 0: Infrastructure — locations created first, then calling enabled
78
+ ("location", "create"): 0,
79
+ ("location", "enable_calling"): 0,
80
+ # Tier 1: Routing backbone — trunks, schedules, route groups, line key templates
81
+ ("trunk", "create"): 1,
82
+ ("route_group", "create"): 1,
83
+ ("route_list", "create"): 1,
84
+ ("route_list", "configure_numbers"): 1,
85
+ ("operating_mode", "create"): 1,
86
+ ("schedule", "create"): 1,
87
+ ("line_key_template", "create"): 1,
88
+ # Tier 2: People creation + org-wide routing objects
89
+ ("user", "create"): 2,
90
+ ("workspace", "create"): 2,
91
+ ("dial_plan", "create"): 2,
92
+ ("translation_pattern", "create"): 2,
93
+ # calling_permission has no "create" op — permissions are per-user, not standalone
94
+ # Tier 3: Numbers, devices (Fix 12: license+number folded into create)
95
+ ("workspace", "assign_number"): 3,
96
+ ("device", "create"): 3,
97
+ ("device", "create_activation_code"): 3,
98
+ # Tier 4: Call features (depend on users/workspaces existing)
99
+ ("hunt_group", "create"): 4,
100
+ ("call_queue", "create"): 4,
101
+ ("auto_attendant", "create"): 4,
102
+ ("call_park", "create"): 4,
103
+ ("pickup_group", "create"): 4,
104
+ ("paging_group", "create"): 4,
105
+ ("voicemail_group", "create"): 4,
106
+ # Tier 5: Settings configuration (depends on licensed users)
107
+ ("user", "configure_settings"): 5,
108
+ ("user", "configure_voicemail"): 5,
109
+ ("workspace", "configure_settings"): 5,
110
+ ("device", "configure_settings"): 5,
111
+ ("calling_permission", "assign"): 5,
112
+ ("call_forwarding", "configure"): 5,
113
+ ("single_number_reach", "configure"): 5,
114
+ # Feature forwarding/holiday/night service configuration
115
+ # (from 2026-04-10-feature-forwarding-night-service.md)
116
+ ("hunt_group", "configure_forwarding"): 5,
117
+ ("call_queue", "configure_forwarding"): 5,
118
+ ("call_queue", "configure_holiday_service"): 5,
119
+ ("call_queue", "configure_night_service"): 5,
120
+ ("call_queue", "configure_stranded_calls"): 5,
121
+ ("auto_attendant", "configure_forwarding"): 5,
122
+ ("ecbn_config", "configure"): 5,
123
+ # Tier 6: Shared/virtual lines + monitoring (depend on users + devices)
124
+ ("shared_line", "configure"): 6,
125
+ ("virtual_line", "create"): 6,
126
+ ("virtual_line", "configure"): 6,
127
+ ("monitoring_list", "configure"): 6,
128
+ ("receptionist_config", "configure"): 6,
129
+ # Tier 7: Device finalization + cycle fixups
130
+ # NOTE: tier 7 is shared between device finalization and cycle-break fixups.
131
+ # Fixups use batch="fixups"; device ops use location-derived batches — no conflict.
132
+ ("device_layout", "configure"): 7,
133
+ ("softkey_config", "configure"): 7,
134
+ # Device settings templates — location-level config at tier 1, per-device overrides at tier 5
135
+ ("device_settings_template", "apply_location_settings"): 1,
136
+ ("device_settings_template", "apply_device_override"): 5,
137
+ # Hoteling / Hot Desking (depends on users + devices)
138
+ ("device_profile", "enable_hoteling_guest"): 5,
139
+ ("device_profile", "enable_hoteling_host"): 5,
140
+ ("hoteling_location", "enable_hotdesking"): 0, # Same tier as location:enable_calling
141
+ # Bulk job operations (Phase: bulk-operations)
142
+ ("bulk_device_settings", "submit"): 5,
143
+ ("bulk_line_key_template", "submit"): 7,
144
+ ("bulk_dynamic_settings", "submit"): 7,
145
+ ("bulk_rebuild_phones", "submit"): 8, # Tier 8: runs after all device finalization
146
+ # DECT network provisioning (depends on location, users/workspaces)
147
+ ("dect_network", "create"): 2, # Create DECT network at location
148
+ ("dect_network", "create_base_stations"): 2, # Register base stations with MACs
149
+ ("dect_network", "assign_handsets"): 3, # Assign handsets (after users/workspaces created)
150
+ # Advisory-to-execution bridge (Phase A: no-op placeholders)
151
+ ("music_on_hold", "configure"): 5, # Settings tier — location must exist + calling enabled
152
+ ("announcement", "upload"): 1, # Routing backbone — features reference announcements
153
+ # Executive/assistant pairing (from 2026-04-10-executive-assistant-migration.md §4e)
154
+ # Type assignments must precede pairing; pairing must precede alert/filtering/screening.
155
+ # Steps 4-7 (alert, filtering, screening, assistant settings) can run in parallel after step 3.
156
+ ("executive_assistant", "assign_executive_type"): 5,
157
+ ("executive_assistant", "assign_assistant_type"): 5,
158
+ ("executive_assistant", "assign_assistants"): 5,
159
+ ("executive_assistant", "configure_alert"): 5,
160
+ ("executive_assistant", "configure_filtering"): 5,
161
+ ("executive_assistant", "configure_screening"): 5,
162
+ ("executive_assistant", "configure_assistant_settings"): 5,
163
+ }
164
+
165
+ # ---------------------------------------------------------------------------
166
+ # API call estimates per operation
167
+ # (from 05-dependency-graph.md lines 268-281)
168
+ # ---------------------------------------------------------------------------
169
+
170
+ API_CALL_ESTIMATES: dict[str, int] = {
171
+ # Tier 0: Infrastructure
172
+ "location:create": 1, # POST /locations (from provisioning.md)
173
+ "location:enable_calling": 1, # POST /telephony/config/locations (Fix 13)
174
+ # Tier 1: Routing backbone + org-wide infrastructure
175
+ "trunk:create": 1, # POST /telephony/config/premisePstn/trunks (from call-routing.md)
176
+ "route_group:create": 1, # POST /telephony/config/premisePstn/routeGroups (from call-routing.md)
177
+ "route_list:create": 1, # POST /telephony/config/premisePstn/routeLists
178
+ "route_list:configure_numbers": 1, # PUT /telephony/config/premisePstn/routeLists/{id}/numbers
179
+ "operating_mode:create": 1, # POST /telephony/config/operatingModes (from location-call-settings-media.md)
180
+ "schedule:create": 1, # POST /telephony/config/locations/{id}/schedules (from location-call-settings-media.md)
181
+ "line_key_template:create": 1, # POST /telephony/config/devices/lineKeyTemplates
182
+ # Tier 2: People + org-wide routing
183
+ "user:create": 1, # POST /people (from provisioning.md) — Fix 12: includes license + extension
184
+ "user:configure_settings": 5, # ~5 PUT calls: callForwarding, callerId, callWaiting, doNotDisturb,
185
+ # + 1 more (monitoring/recording/intercept) (from person-call-settings-*.md)
186
+ "user:configure_voicemail": 1, # PUT /people/{id}/features/voicemail (from person-call-settings-media.md)
187
+ "workspace:create": 1, # POST /workspaces with calling config (from devices-workspaces.md)
188
+ "workspace:assign_number": 1, # PUT number assignment if DID needed beyond extension in create
189
+ # (from devices-workspaces.md Raw HTTP section)
190
+ "workspace:configure_settings": 3, # ~3 PUT calls: callForwarding, callWaiting, callerId
191
+ # (from devices-workspaces.md Workspace Call Settings section)
192
+ "device:create": 1, # POST /devices by MAC or POST /devices/activationCode
193
+ # (from devices-core.md lines 1312-1323, 1304-1308)
194
+ "device:create_activation_code": 1, # POST /devices/activationCode (from devices-core.md)
195
+ "device:configure_settings": 1, # PUT /telephony/config/devices/{id}/settings (from devices-core.md)
196
+ "dial_plan:create": 1, # POST /telephony/config/premisePstn/dialPlans (from call-routing.md)
197
+ "translation_pattern:create": 1, # POST /telephony/config/callRouting/translationPatterns (from call-routing.md)
198
+ "calling_permission:create": 0, # logical grouping only — no standalone API (from person-call-settings-permissions.md)
199
+ "calling_permission:assign": 1, # PUT /people/{id}/features/outgoingPermission per user
200
+ # (from person-call-settings-permissions.md line 401)
201
+ "call_forwarding:configure": 1, # PUT /people/{id}/features/callForwarding
202
+ "single_number_reach:configure": 2, # PUT SNR enable + POST per number
203
+ # Feature forwarding/holiday/night service configuration
204
+ "hunt_group:configure_forwarding": 1, # PUT .../huntGroups/{id}/callForwarding
205
+ "call_queue:configure_forwarding": 1, # PUT .../queues/{id}/callForwarding
206
+ "call_queue:configure_holiday_service": 1, # PUT .../queues/{id}/holidayService
207
+ "call_queue:configure_night_service": 1, # PUT .../queues/{id}/nightService
208
+ "call_queue:configure_stranded_calls": 1, # PUT .../queues/{id}/strandedCalls
209
+ "auto_attendant:configure_forwarding": 1, # PUT .../autoAttendants/{id}/callForwarding
210
+ "ecbn_config:configure": 1, # PUT /telephony/config/{kind}/{id}/emergencyCallbackNumber
211
+ # Tier 4: Call features
212
+ "hunt_group:create": 1, # POST /telephony/config/locations/{id}/huntGroups
213
+ # agents optional at create (from call-features-major.md line 894)
214
+ "call_queue:create": 1, # POST /telephony/config/locations/{id}/queues
215
+ # (from call-features-major.md lines 616-627)
216
+ "auto_attendant:create": 1, # POST /telephony/config/locations/{id}/autoAttendants — menu is part of
217
+ # the create body, not a separate PUT (from call-features-major.md line 184)
218
+ "call_park:create": 1, # POST /telephony/config/locations/{id}/callParks (from call-features-additional.md)
219
+ "pickup_group:create": 1, # POST /telephony/config/locations/{id}/callPickups (from call-features-additional.md)
220
+ "paging_group:create": 1, # POST /telephony/config/locations/{id}/paging (from call-features-additional.md)
221
+ "voicemail_group:create": 1, # POST /telephony/config/locations/{id}/voicemailGroups
222
+ # Tier 6: Shared/virtual lines + monitoring
223
+ "shared_line:configure": 2, # Approximate — one PUT per owner to /people/{id}/applications/members
224
+ # (from person-call-settings-behavior.md §4)
225
+ "virtual_line:create": 1, # POST /telephony/config/virtualLines (from virtual-lines.md lines 93-104)
226
+ "virtual_line:configure": 1, # PUT /telephony/config/virtualLines/{id} (from virtual-lines.md lines 132-147)
227
+ "monitoring_list:configure": 1,
228
+ "receptionist_config:configure": 2, # PUT reception + POST directory # PUT /people/{id}/features/monitoring
229
+ # Tier 7: Device finalization
230
+ "device_layout:configure": 3, # PUT members + PUT layout + POST applyChanges
231
+ "softkey_config:configure": 2, # PUT dynamicSettings + POST applyChanges
232
+ # Device settings templates
233
+ "device_settings_template:apply_location_settings": 1, # PUT /telephony/config/locations/{id}/devices/settings
234
+ "device_settings_template:apply_device_override": 1, # PUT /telephony/config/devices/{id}/settings
235
+ # Hoteling / Hot Desking
236
+ "device_profile:enable_hoteling_guest": 1, # PUT /people/{id}/features/hoteling
237
+ "device_profile:enable_hoteling_host": 1, # PUT /telephony/config/people/{id}/devices/settings/hoteling
238
+ "hoteling_location:enable_hotdesking": 1, # PUT /telephony/config/locations/{id}/features/hotDesking
239
+ # Bulk job operations (submit=1 + ~5-10 polling calls; conservatively estimated at 10)
240
+ "bulk_device_settings:submit": 10,
241
+ "bulk_line_key_template:submit": 10,
242
+ "bulk_dynamic_settings:submit": 10,
243
+ "bulk_rebuild_phones:submit": 10,
244
+ # DECT network provisioning
245
+ "dect_network:create": 1, # POST /telephony/config/locations/{id}/dectNetworks
246
+ "dect_network:create_base_stations": 1, # POST /telephony/config/locations/{id}/dectNetworks/{id}/baseStations
247
+ "dect_network:assign_handsets": 1, # POST /telephony/config/locations/{id}/dectNetworks/{id}/handsets
248
+ # Advisory-to-execution bridge (Phase A: no-op placeholders, 0 API calls)
249
+ "music_on_hold:configure": 0,
250
+ "announcement:upload": 0,
251
+ # Executive/assistant pairing
252
+ "executive_assistant:assign_executive_type": 1, # PUT /people/{id}/features/executiveAssistant
253
+ "executive_assistant:assign_assistant_type": 2, # PUT per assistant (typically 1-3)
254
+ "executive_assistant:assign_assistants": 1, # PUT /telephony/config/people/{id}/executive/assignedAssistants
255
+ "executive_assistant:configure_alert": 1, # PUT /telephony/config/people/{id}/executive/alert
256
+ "executive_assistant:configure_filtering": 1, # PUT /telephony/config/people/{id}/executive/callFiltering
257
+ "executive_assistant:configure_screening": 1, # PUT /telephony/config/people/{id}/executive/screening
258
+ "executive_assistant:configure_assistant_settings": 2, # PUT per assistant /executive/assistant
259
+ }
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Org-wide resource types — these go into the "org-wide" batch, not per-site
263
+ # (from 05-dependency-graph.md batch ordering lines 206-216)
264
+ # ---------------------------------------------------------------------------
265
+
266
+ ORG_WIDE_TYPES: set[str] = {
267
+ "location",
268
+ "trunk",
269
+ "route_group",
270
+ "route_list",
271
+ "operating_mode",
272
+ "dial_plan",
273
+ "translation_pattern",
274
+ "calling_permission",
275
+ "line_key_template", # Org-scoped; not tied to a specific location
276
+ }
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Bulk job support
280
+ # (from docs/superpowers/specs/2026-04-10-bulk-operations.md §3a, §4c)
281
+ # ---------------------------------------------------------------------------
282
+
283
+ BULK_DEVICE_THRESHOLD_DEFAULT: int = 100
284
+
285
+ # Resource types whose ops must run sequentially within a tier, not via
286
+ # asyncio.gather(). Driven by Webex's one-job-per-org constraint for these
287
+ # job families (spec §4c, §5a).
288
+ SERIALIZED_RESOURCE_TYPES: set[str] = {
289
+ "bulk_device_settings",
290
+ "bulk_line_key_template",
291
+ "bulk_dynamic_settings",
292
+ "bulk_rebuild_phones",
293
+ }
294
+
295
+ # Maps internal bulk resource_type to the Webex jobType path segment used
296
+ # for polling and error-fetch URLs:
297
+ # GET /v1/telephony/config/jobs/devices/{jobType}/{jobId}[/errors]
298
+ BULK_JOB_TYPES: dict[str, str] = {
299
+ "bulk_device_settings": "callDeviceSettings",
300
+ "bulk_line_key_template": "applyLineKeyTemplate",
301
+ "bulk_dynamic_settings": "dynamicDeviceSettings",
302
+ "bulk_rebuild_phones": "rebuildPhones",
303
+ }
@@ -0,0 +1,277 @@
1
+ """Batch partitioning — split the DAG into executable batches.
2
+
3
+ Two-level partitioning:
4
+ 1. Org-wide vs site-specific (based on batch assignment on each node)
5
+ 2. Within each group, split by tier
6
+
7
+ Batch ordering:
8
+ 1. org-wide tiers 0-1 (locations, trunks, schedules, route groups)
9
+ 2. org-wide tier 2 (dial plans, calling permissions)
10
+ 3. Per-site tiers 2-6 (users, devices, features, settings)
11
+ 4. Fixups tier 7 (broken cycle fixups)
12
+
13
+ (from 05-dependency-graph.md lines 202-261)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import logging
20
+ import math
21
+ from collections import defaultdict
22
+
23
+ import networkx as nx
24
+
25
+ from wxcli.migration.execute import Batch, DependencyType
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def partition_into_batches(
31
+ G: nx.DiGraph,
32
+ rate_limit_per_minute: int = 100,
33
+ ) -> list[Batch]:
34
+ """Partition the DAG into executable batches.
35
+
36
+ Batches are ordered: org-wide first, then per-site, then fixups.
37
+ Within each group, split by tier. Returns a list of Batch objects
38
+ with estimated API call counts and timing.
39
+
40
+ (from 05-dependency-graph.md lines 221-258)
41
+ """
42
+ # Separate nodes by batch assignment
43
+ org_wide_nodes: list[str] = []
44
+ fixup_nodes: list[str] = []
45
+ site_nodes: dict[str, list[str]] = defaultdict(list)
46
+
47
+ for node in nx.topological_sort(G):
48
+ batch = G.nodes[node].get("batch")
49
+
50
+ if batch == "fixups":
51
+ fixup_nodes.append(node)
52
+ elif batch is None or batch == "org-wide":
53
+ org_wide_nodes.append(node)
54
+ else:
55
+ site_nodes[batch].append(node)
56
+
57
+ batches: list[Batch] = []
58
+
59
+ # 1. Org-wide batches first, split by tier
60
+ batches.extend(_split_by_tier(org_wide_nodes, G, "org-wide"))
61
+
62
+ # 2. Per-site batches, sorted by site name for determinism
63
+ for site in sorted(site_nodes.keys()):
64
+ batches.extend(_split_by_tier(site_nodes[site], G, site))
65
+
66
+ # 3. Fixup batch last
67
+ if fixup_nodes:
68
+ batches.extend(_split_by_tier(fixup_nodes, G, "fixups"))
69
+
70
+ # Log summary
71
+ total_ops = sum(len(b.operations) for b in batches)
72
+ total_api = sum(b.estimated_api_calls for b in batches)
73
+ logger.info(
74
+ "Partitioned %d operations into %d batches "
75
+ "(estimated %d API calls, ~%d min at %d req/min)",
76
+ total_ops,
77
+ len(batches),
78
+ total_api,
79
+ math.ceil(total_api / rate_limit_per_minute) if total_api else 0,
80
+ rate_limit_per_minute,
81
+ )
82
+
83
+ return batches
84
+
85
+
86
+ def _split_by_tier(
87
+ nodes: list[str],
88
+ G: nx.DiGraph,
89
+ site: str,
90
+ ) -> list[Batch]:
91
+ """Split a set of nodes into one batch per tier.
92
+
93
+ (from 05-dependency-graph.md lines 244-258)
94
+ """
95
+ by_tier: dict[int, list[str]] = defaultdict(list)
96
+ for n in nodes:
97
+ tier = G.nodes[n].get("tier", 0)
98
+ by_tier[tier].append(n)
99
+
100
+ batches = []
101
+ for tier in sorted(by_tier.keys()):
102
+ tier_nodes = by_tier[tier]
103
+ api_calls = sum(G.nodes[n].get("api_calls", 1) for n in tier_nodes)
104
+ batches.append(Batch(
105
+ site=site,
106
+ tier=tier,
107
+ operations=tier_nodes,
108
+ estimated_api_calls=api_calls,
109
+ ))
110
+
111
+ return batches
112
+
113
+
114
+ def format_batch_plan(
115
+ batches: list[Batch],
116
+ rate_limit_per_minute: int = 100,
117
+ ) -> str:
118
+ """Format a human-readable execution plan from batches.
119
+
120
+ (from 05-dependency-graph.md lines 285-295 — plan output format)
121
+ """
122
+ if not batches:
123
+ return "No batches to execute."
124
+
125
+ lines: list[str] = ["Execution Plan", "=" * 60]
126
+
127
+ total_api = 0
128
+ for batch in batches:
129
+ total_api += batch.estimated_api_calls
130
+ est_minutes = math.ceil(batch.estimated_api_calls / rate_limit_per_minute) if batch.estimated_api_calls else 0
131
+
132
+ # Count operations by type
133
+ op_counts: dict[str, int] = defaultdict(int)
134
+ for node_id in batch.operations:
135
+ # node_id format: "canonical_id:op_type"
136
+ parts = node_id.rsplit(":", 1)
137
+ if len(parts) == 2:
138
+ op_counts[parts[1]] += 1
139
+ else:
140
+ op_counts["unknown"] += 1
141
+
142
+ op_summary = ", ".join(
143
+ f"{count} {op_type}" for op_type, count in sorted(op_counts.items())
144
+ )
145
+
146
+ lines.append(f"\nBatch: {batch.site} / tier {batch.tier}")
147
+ lines.append(f" Operations: {op_summary}")
148
+ lines.append(f" Estimated API calls: {batch.estimated_api_calls}")
149
+ if est_minutes > 0:
150
+ lines.append(f" Estimated time at {rate_limit_per_minute} req/min: ~{est_minutes} minutes")
151
+
152
+ total_minutes = math.ceil(total_api / rate_limit_per_minute) if total_api else 0
153
+ lines.append(f"\n{'=' * 60}")
154
+ lines.append(f"Total: {sum(len(b.operations) for b in batches)} operations, "
155
+ f"{total_api} API calls, ~{total_minutes} minutes")
156
+
157
+ return "\n".join(lines)
158
+
159
+
160
+ def save_plan_to_store(
161
+ G: nx.DiGraph,
162
+ store: "MigrationStore",
163
+ ) -> None:
164
+ """Persist the execution plan (operations + edges) to SQLite.
165
+
166
+ Writes to plan_operations and plan_edges tables. Clears any
167
+ existing plan data first (idempotent re-planning).
168
+
169
+ (from 05-dependency-graph.md lines 300-327 — plan_operations + plan_edges tables)
170
+ """
171
+ conn = store.conn
172
+
173
+ # Clear existing plan (edges first — FK constraint)
174
+ conn.execute("DELETE FROM plan_edges")
175
+ conn.execute("DELETE FROM plan_operations")
176
+
177
+ # Write operations
178
+ for node_id in G.nodes():
179
+ node = G.nodes[node_id]
180
+ payload = node.get("payload")
181
+ data_json = json.dumps(payload) if payload else None
182
+ cid = node.get("canonical_id", "")
183
+ # Bulk ops have synthetic canonical_ids not in the objects table.
184
+ # Insert a placeholder row so the FK constraint is satisfied.
185
+ if payload and cid:
186
+ conn.execute(
187
+ """INSERT OR IGNORE INTO objects
188
+ (canonical_id, object_type, status, data, created_at, updated_at)
189
+ VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))""",
190
+ (cid, node.get("resource_type", "bulk_job"), "analyzed",
191
+ data_json),
192
+ )
193
+ conn.execute(
194
+ """INSERT INTO plan_operations
195
+ (node_id, canonical_id, op_type, resource_type, tier, batch,
196
+ api_calls, description, status, data_json)
197
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
198
+ (
199
+ node_id,
200
+ node.get("canonical_id", ""),
201
+ node.get("op_type", ""),
202
+ node.get("resource_type", ""),
203
+ node.get("tier", 0),
204
+ node.get("batch"),
205
+ node.get("api_calls", 1),
206
+ node.get("description", ""),
207
+ "pending",
208
+ data_json,
209
+ ),
210
+ )
211
+
212
+ # Write edges
213
+ for u, v, data in G.edges(data=True):
214
+ dep_type = data.get("type", DependencyType.CONFIGURES)
215
+ dep_type_str = dep_type.value if hasattr(dep_type, "value") else str(dep_type)
216
+ conn.execute(
217
+ """INSERT OR IGNORE INTO plan_edges (from_node, to_node, dep_type, broken)
218
+ VALUES (?, ?, ?, ?)""",
219
+ (u, v, dep_type_str, 0),
220
+ )
221
+
222
+ conn.commit()
223
+
224
+ total_ops = G.number_of_nodes()
225
+ total_edges = G.number_of_edges()
226
+ logger.info("Saved plan to SQLite: %d operations, %d edges", total_ops, total_edges)
227
+
228
+
229
+ def load_plan_from_store(store: "MigrationStore") -> nx.DiGraph:
230
+ """Reconstruct the NetworkX DiGraph from SQLite plan tables.
231
+
232
+ The tables are the source of truth; the in-memory graph is a working view.
233
+
234
+ (from 05-dependency-graph.md line 328)
235
+ """
236
+ G = nx.DiGraph()
237
+ conn = store.conn
238
+
239
+ # Load nodes
240
+ rows = conn.execute(
241
+ """SELECT node_id, canonical_id, op_type, resource_type, tier, batch,
242
+ api_calls, description, status
243
+ FROM plan_operations"""
244
+ ).fetchall()
245
+ for row in rows:
246
+ G.add_node(
247
+ row["node_id"],
248
+ canonical_id=row["canonical_id"],
249
+ op_type=row["op_type"],
250
+ resource_type=row["resource_type"],
251
+ tier=row["tier"],
252
+ batch=row["batch"],
253
+ api_calls=row["api_calls"],
254
+ description=row["description"],
255
+ status=row["status"],
256
+ )
257
+
258
+ # Load edges
259
+ edge_rows = conn.execute(
260
+ "SELECT from_node, to_node, dep_type, broken FROM plan_edges"
261
+ ).fetchall()
262
+ for row in edge_rows:
263
+ _valid_values = {e.value for e in DependencyType}
264
+ dep_type = DependencyType(row["dep_type"]) if row["dep_type"] in _valid_values else row["dep_type"]
265
+ G.add_edge(
266
+ row["from_node"],
267
+ row["to_node"],
268
+ type=dep_type,
269
+ broken=bool(row["broken"]),
270
+ )
271
+
272
+ logger.info(
273
+ "Loaded plan from SQLite: %d operations, %d edges",
274
+ G.number_of_nodes(),
275
+ G.number_of_edges(),
276
+ )
277
+ return G