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,310 @@
1
+ """Line mapper: CUCM Directory Numbers -> Webex Phone Numbers + Extensions.
2
+
3
+ Maps CUCM DNs (identified from device line appearances) to CanonicalLine objects
4
+ with E.164 normalization. Each unique DN (pattern + partition) becomes one
5
+ CanonicalLine.
6
+
7
+ (from 03b-transform-mappers.md §3, line_mapper)
8
+
9
+ Cross-ref reads:
10
+ device_has_dn (Device -> DN) — reverse-queried to find devices per DN
11
+ dn_in_partition (DN -> Partition)
12
+ device_in_pool (Device -> DevicePool) — for location chain
13
+ device_pool_to_location (DevicePool -> Location) — for country code
14
+
15
+ Does NOT produce: EXTENSION_CONFLICT (analyzer-owned)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from datetime import datetime, timezone
22
+ from typing import Any
23
+
24
+ from wxcli.migration.models import (
25
+ CanonicalLine,
26
+ DecisionType,
27
+ LineClassification,
28
+ MapperResult,
29
+ MigrationStatus,
30
+ Provenance,
31
+ )
32
+ from wxcli.migration.store import MigrationStore
33
+ from wxcli.migration.transform.e164 import normalize_dn
34
+ from wxcli.migration.transform.mappers.base import (
35
+ Mapper,
36
+ decision_to_store_dict,
37
+ manual_option,
38
+ skip_option,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Webex extension length constraints
44
+ # (from 03b-transform-mappers.md §3: "Extensions are 2-10 chars in Webex")
45
+ _MIN_EXTENSION_LENGTH = 2
46
+ _MAX_EXTENSION_LENGTH = 10
47
+
48
+
49
+ class LineMapper(Mapper):
50
+ """Maps CUCM Directory Numbers to Webex Phone Numbers and Extensions.
51
+
52
+ (from 03b-transform-mappers.md §3)
53
+ """
54
+
55
+ name = "line_mapper"
56
+ depends_on = ["location_mapper"]
57
+
58
+ def __init__(
59
+ self,
60
+ default_country_code: str = "US",
61
+ site_prefix_rules: list[dict] | None = None,
62
+ ) -> None:
63
+ """
64
+ Args:
65
+ default_country_code: ISO 3166-1 alpha-2 code for E.164 normalization
66
+ (from 03b-transform-mappers.md §3: country code from location)
67
+ site_prefix_rules: List of prefix stripping rules from migration config
68
+ """
69
+ self.default_country_code = default_country_code
70
+ self.site_prefix_rules = site_prefix_rules or []
71
+
72
+ def map(self, store: MigrationStore) -> MapperResult:
73
+ """Find all unique DNs from device line appearances and produce CanonicalLine objects."""
74
+ result = MapperResult()
75
+
76
+ # Collect all unique DN IDs from device_has_dn cross-refs
77
+ all_dn_refs = store.get_cross_refs(relationship="device_has_dn")
78
+ unique_dn_ids: set[str] = set()
79
+ # Track which devices reference each DN (for shared line detection)
80
+ dn_to_devices: dict[str, list[str]] = {}
81
+ for ref in all_dn_refs:
82
+ dn_id = ref["to_id"]
83
+ dev_id = ref["from_id"]
84
+ unique_dn_ids.add(dn_id)
85
+ dn_to_devices.setdefault(dn_id, []).append(dev_id)
86
+
87
+ # Build index of pre-computed e164_results from device line appearances
88
+ # (from Phase 04 corrections: CrossReferenceBuilder computes e164_result
89
+ # on each line appearance — the line_mapper can read this directly)
90
+ precomputed_e164: dict[str, dict[str, Any]] = {}
91
+ self._build_precomputed_e164_index(store, dn_to_devices, precomputed_e164)
92
+
93
+ for dn_id in sorted(unique_dn_ids):
94
+ # Parse DN ID format: "dn:pattern:partition"
95
+ parts = dn_id.split(":", 2)
96
+ if len(parts) < 2:
97
+ continue
98
+ dn_pattern = parts[1]
99
+ partition = parts[2] if len(parts) > 2 else "<None>"
100
+ country_code: str | None = None
101
+
102
+ # Check for pre-computed e164_result from line appearances
103
+ # (from Phase 04 corrections: use e164_result when available)
104
+ cached = precomputed_e164.get(dn_id)
105
+ if cached:
106
+ e164_result_data = cached
107
+ # Build an E164Result-compatible object from the cached dict
108
+ e164_result = _E164ResultProxy(
109
+ e164=e164_result_data.get("e164"),
110
+ extension=e164_result_data.get("extension"),
111
+ raw=dn_pattern,
112
+ classification=e164_result_data.get("classification", "AMBIGUOUS"),
113
+ )
114
+ else:
115
+ # Fallback: resolve country code and run normalize_dn()
116
+ country_code = self._resolve_country_code(store, dn_id, dn_to_devices)
117
+ e164_result = normalize_dn(
118
+ dn_pattern, country_code, self.site_prefix_rules
119
+ )
120
+
121
+ # Shared line detection
122
+ # (from 03b-transform-mappers.md §3: tag shared:true when multiple devices)
123
+ device_list = dn_to_devices.get(dn_id, [])
124
+ unique_devices = list(set(device_list))
125
+ shared = len(unique_devices) > 1
126
+
127
+ # Map classification string to enum
128
+ classification = _classify(e164_result.classification)
129
+
130
+ # Build extension and e164 fields
131
+ extension = None
132
+ e164 = None
133
+
134
+ if classification == LineClassification.EXTENSION:
135
+ extension = e164_result.extension or dn_pattern
136
+ elif classification in (LineClassification.NATIONAL, LineClassification.E164):
137
+ e164 = e164_result.e164
138
+ # For national/E164 numbers, the raw pattern may also serve as extension
139
+ # if it looks like one. But per the design, extension comes from the
140
+ # classified result — NATIONAL/E164 numbers don't get an extension field
141
+ # unless separately configured. The user_mapper gets bare extension from
142
+ # user_has_primary_dn independently.
143
+ # However: a 10-digit national number IS the extension in many CUCM deployments
144
+ # (from 03b-transform-mappers.md §3: "If classified as EXTENSION: stored as bare extension")
145
+ extension = e164_result.extension
146
+ elif classification == LineClassification.AMBIGUOUS:
147
+ # DN can't be classified
148
+ # (from 03b-transform-mappers.md §3: DN_AMBIGUOUS decision)
149
+ decision = self._create_decision(
150
+ store=store,
151
+ decision_type=DecisionType.DN_AMBIGUOUS,
152
+ severity="MEDIUM",
153
+ summary=f"DN '{dn_pattern}' (partition: {partition}) cannot be classified as extension, national, or E.164",
154
+ context={
155
+ "dn_id": dn_id,
156
+ "dn_pattern": dn_pattern,
157
+ "partition": partition,
158
+ "country_code": country_code,
159
+ },
160
+ options=[
161
+ manual_option("Admin classifies as extension-only"),
162
+ manual_option("Admin provides correct E.164"),
163
+ skip_option("Number not migrated"),
164
+ ],
165
+ affected_objects=[dn_id],
166
+ )
167
+ store.save_decision(decision_to_store_dict(decision))
168
+ result.decisions.append(decision)
169
+ continue
170
+
171
+ # Validate extension length for Webex
172
+ # (from 03b-transform-mappers.md §3: "extensions outside 2-10 chars produce MISSING_DATA")
173
+ if extension and (
174
+ len(extension) < _MIN_EXTENSION_LENGTH
175
+ or len(extension) > _MAX_EXTENSION_LENGTH
176
+ ):
177
+ decision = self._create_decision(
178
+ store=store,
179
+ decision_type=DecisionType.MISSING_DATA,
180
+ severity="MEDIUM",
181
+ summary=f"DN '{dn_pattern}' resolves to extension '{extension}' which is outside Webex's 2-10 char limit",
182
+ context={
183
+ "dn_id": dn_id,
184
+ "dn_pattern": dn_pattern,
185
+ "extension": extension,
186
+ "extension_length": len(extension),
187
+ "partition": partition,
188
+ },
189
+ options=[
190
+ manual_option("Admin provides valid extension (2-10 chars)"),
191
+ skip_option("Number not migrated"),
192
+ ],
193
+ affected_objects=[dn_id],
194
+ )
195
+ store.save_decision(decision_to_store_dict(decision))
196
+ result.decisions.append(decision)
197
+ # Still create the line object but flag it
198
+ # (user can resolve the decision later)
199
+
200
+ line = CanonicalLine(
201
+ canonical_id=f"line:{dn_pattern}:{partition}",
202
+ provenance=Provenance(
203
+ source_system="cucm",
204
+ source_id=dn_id,
205
+ source_name=dn_pattern,
206
+ extracted_at=datetime.now(timezone.utc),
207
+ ),
208
+ status=MigrationStatus.ANALYZED,
209
+ extension=extension,
210
+ e164=e164,
211
+ classification=classification,
212
+ shared=shared,
213
+ cucm_pattern=dn_pattern,
214
+ route_partition_name=partition if partition != "<None>" else None,
215
+ )
216
+
217
+ store.upsert_object(line)
218
+ result.objects_created += 1
219
+
220
+ return result
221
+
222
+ def _build_precomputed_e164_index(
223
+ self,
224
+ store: MigrationStore,
225
+ dn_to_devices: dict[str, list[str]],
226
+ precomputed: dict[str, dict[str, Any]],
227
+ ) -> None:
228
+ """Build index of pre-computed e164_results from device line appearances.
229
+
230
+ After CrossReferenceBuilder runs, each line appearance entry in a device's
231
+ pre_migration_state has e164_result: {e164, extension, classification}.
232
+ We index these by DN ID so the mapper can use them directly.
233
+
234
+ (from Phase 04 corrections: line appearances have e164_result already computed)
235
+ """
236
+ seen_devices: set[str] = set()
237
+ for dn_id, device_ids in dn_to_devices.items():
238
+ if dn_id in precomputed:
239
+ continue
240
+ for dev_id in device_ids:
241
+ if dev_id in seen_devices:
242
+ continue
243
+ seen_devices.add(dev_id)
244
+ # Handle virtual cross-ref IDs gracefully
245
+ # (from Phase 04 corrections: get_object may return None)
246
+ dev_data = store.get_object(dev_id)
247
+ if not dev_data:
248
+ continue
249
+ state = dev_data.get("pre_migration_state") or {}
250
+ for la in state.get("line_appearances", []):
251
+ la_dn_id = la.get("dn_canonical_id")
252
+ e164_res = la.get("e164_result")
253
+ if la_dn_id and e164_res and la_dn_id not in precomputed:
254
+ precomputed[la_dn_id] = e164_res
255
+
256
+ def _resolve_country_code(
257
+ self,
258
+ store: MigrationStore,
259
+ dn_id: str,
260
+ dn_to_devices: dict[str, list[str]],
261
+ ) -> str:
262
+ """Resolve country code for a DN by following the location chain.
263
+
264
+ Chain: DN -> (reverse device_has_dn) -> device -> device_in_pool
265
+ -> device_pool_to_location -> location -> address.country
266
+
267
+ (from 03b-transform-mappers.md §3: "country_code comes from the DN's resolved location")
268
+
269
+ Handles virtual cross-ref IDs gracefully — if get_object returns None
270
+ for any step, continues to the next device.
271
+ (from Phase 04 corrections: handle get_object returning None)
272
+ """
273
+ device_ids = dn_to_devices.get(dn_id, [])
274
+ for dev_id in device_ids:
275
+ # Follow: device -> device_in_pool -> device_pool_to_location
276
+ location_id = store.resolve_chain(
277
+ dev_id, "device_in_pool", "device_pool_to_location"
278
+ )
279
+ if location_id:
280
+ loc_data = store.get_object(location_id)
281
+ if loc_data:
282
+ address = loc_data.get("address", {})
283
+ country = address.get("country")
284
+ if country:
285
+ return country
286
+ return self.default_country_code
287
+
288
+
289
+ class _E164ResultProxy:
290
+ """Lightweight proxy matching E164Result interface for pre-computed values.
291
+
292
+ Used when e164_result is already available from CrossReferenceBuilder
293
+ rather than calling normalize_dn().
294
+ """
295
+
296
+ __slots__ = ("e164", "extension", "raw", "classification")
297
+
298
+ def __init__(self, e164: str | None, extension: str | None, raw: str, classification: str):
299
+ self.e164 = e164
300
+ self.extension = extension
301
+ self.raw = raw
302
+ self.classification = classification
303
+
304
+
305
+ def _classify(classification_str: str) -> LineClassification:
306
+ """Convert E164Result classification string to LineClassification enum."""
307
+ try:
308
+ return LineClassification(classification_str)
309
+ except ValueError:
310
+ return LineClassification.AMBIGUOUS
@@ -0,0 +1,235 @@
1
+ """Location mapper: CUCM Device Pools -> Webex Calling Locations.
2
+
3
+ Maps one or more CUCM device pools to a single CanonicalLocation, consolidated
4
+ by the CUCM Location entity they reference. Multiple device pools sharing the
5
+ same CUCM Location produce exactly one Webex location.
6
+
7
+ (from 03b-transform-mappers.md §1, location_mapper)
8
+
9
+ Cross-ref reads:
10
+ device_pool_has_datetime_group (DevicePool -> DateTimeGroup) — timezone
11
+ device_pool_at_cucm_location (DevicePool -> CUCM Location) — address
12
+
13
+ Cross-ref writes:
14
+ device_pool_to_location (DevicePool canonical_id -> new location canonical_id)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import logging
21
+ from datetime import datetime, timezone
22
+ from typing import Any
23
+
24
+ from wxcli.migration.models import (
25
+ CanonicalLocation,
26
+ DecisionType,
27
+ LocationAddress,
28
+ MapperResult,
29
+ MigrationStatus,
30
+ Provenance,
31
+ )
32
+ from wxcli.migration.store import MigrationStore
33
+ from wxcli.migration.transform.mappers.base import (
34
+ Mapper,
35
+ decision_to_store_dict,
36
+ manual_option,
37
+ skip_option,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ # Max location name length in Webex
43
+ # (from 03b-transform-mappers.md §1: "truncate to 80 chars")
44
+ _MAX_LOCATION_NAME_LENGTH = 80
45
+
46
+
47
+ class LocationMapper(Mapper):
48
+ """Maps CUCM Device Pools to Webex Calling Locations.
49
+
50
+ (from 03b-transform-mappers.md §1)
51
+ """
52
+
53
+ name = "location_mapper"
54
+ depends_on: list[str] = []
55
+
56
+ def __init__(
57
+ self,
58
+ default_language: str = "en_us",
59
+ default_country: str = "US",
60
+ outside_dial_digit: str = "9",
61
+ site_prefix_rules: list[dict[str, str]] | None = None,
62
+ ) -> None:
63
+ self.default_language = default_language
64
+ self.default_country = default_country
65
+ self.outside_dial_digit = outside_dial_digit
66
+ self.site_prefix_rules = site_prefix_rules or []
67
+
68
+ def map(self, store: MigrationStore) -> MapperResult:
69
+ """Read device pools and produce CanonicalLocation objects.
70
+
71
+ Consolidation: device pools that share a CUCM Location entity are merged
72
+ into a single CanonicalLocation. The first device pool name is used as the
73
+ location name; all pool names are stored in cucm_device_pool_names.
74
+
75
+ (from 03b-transform-mappers.md §1, consolidation edge case)
76
+ """
77
+ result = MapperResult()
78
+
79
+ # Phase 1: Group device pools by their CUCM Location reference
80
+ # Key = cucm_location canonical_id, Value = list of device pool dicts
81
+ location_groups: dict[str, list[dict[str, Any]]] = {}
82
+ # Track device pools with no CUCM location reference
83
+ orphan_pools: list[dict[str, Any]] = []
84
+
85
+ for dp_data in store.get_objects("device_pool"):
86
+ dp_id = dp_data["canonical_id"]
87
+
88
+ # Resolve CUCM Location via cross-ref
89
+ loc_refs = store.find_cross_refs(dp_id, "device_pool_at_cucm_location")
90
+ if loc_refs:
91
+ cucm_loc_id = loc_refs[0]
92
+ location_groups.setdefault(cucm_loc_id, []).append(dp_data)
93
+ else:
94
+ orphan_pools.append(dp_data)
95
+
96
+ # Phase 2: Create one CanonicalLocation per CUCM Location group
97
+ for cucm_loc_id, dp_list in location_groups.items():
98
+ location, warnings = self._build_location(store, cucm_loc_id, dp_list)
99
+ for w in warnings:
100
+ location.warnings.append(w)
101
+
102
+ store.upsert_object(location)
103
+ result.objects_created += 1
104
+
105
+ # Write device_pool_to_location cross-refs for all pools in the group
106
+ for dp_data in dp_list:
107
+ store.add_cross_ref(
108
+ dp_data["canonical_id"],
109
+ location.canonical_id,
110
+ "device_pool_to_location",
111
+ )
112
+
113
+ # Phase 3: Handle orphan device pools (no CUCM Location ref)
114
+ for dp_data in orphan_pools:
115
+ dp_id = dp_data["canonical_id"]
116
+ dp_name = dp_data.get("pre_migration_state", {}).get(
117
+ "device_pool_name", dp_id
118
+ )
119
+
120
+ decision = self._create_decision(
121
+ store=store,
122
+ decision_type=DecisionType.LOCATION_AMBIGUOUS,
123
+ severity="HIGH",
124
+ summary=f"Device pool '{dp_name}' has no CUCM Location reference — cannot determine Webex location",
125
+ context={
126
+ "device_pool_id": dp_id,
127
+ "device_pool_name": dp_name,
128
+ },
129
+ options=[
130
+ manual_option("Admin must provide address and create location manually"),
131
+ skip_option("Device pool not migrated; users/devices in this pool need reassignment"),
132
+ ],
133
+ affected_objects=[dp_id],
134
+ )
135
+ store.save_decision(decision_to_store_dict(decision))
136
+ result.decisions.append(decision)
137
+
138
+ return result
139
+
140
+ def _build_location(
141
+ self,
142
+ store: MigrationStore,
143
+ cucm_loc_id: str,
144
+ dp_list: list[dict[str, Any]],
145
+ ) -> tuple[CanonicalLocation, list[str]]:
146
+ """Build a CanonicalLocation from a group of device pools sharing a CUCM Location.
147
+
148
+ Returns the location and a list of warning strings.
149
+ """
150
+ warnings: list[str] = []
151
+
152
+ # Use first device pool as primary (for naming)
153
+ primary_dp = dp_list[0]
154
+ primary_dp_state = primary_dp.get("pre_migration_state", {})
155
+ dp_names = [
156
+ dp.get("pre_migration_state", {}).get("device_pool_name", dp["canonical_id"])
157
+ for dp in dp_list
158
+ ]
159
+
160
+ # Location name from primary device pool, truncated to 80 chars
161
+ # (from 03b-transform-mappers.md §1: "truncate to 80 chars")
162
+ name = dp_names[0]
163
+ if len(name) > _MAX_LOCATION_NAME_LENGTH:
164
+ warnings.append(
165
+ f"Location name truncated from {len(name)} to {_MAX_LOCATION_NAME_LENGTH} chars: '{name}'"
166
+ )
167
+ name = name[:_MAX_LOCATION_NAME_LENGTH]
168
+
169
+ # Resolve timezone via device_pool_has_datetime_group cross-ref
170
+ # (from 03b-transform-mappers.md §1: datetime group -> IANA timezone)
171
+ time_zone = None
172
+ dtg_name = None
173
+ dtg_refs = store.find_cross_refs(
174
+ primary_dp["canonical_id"], "device_pool_has_datetime_group"
175
+ )
176
+ if dtg_refs:
177
+ dtg_data = store.get_object(dtg_refs[0])
178
+ if dtg_data:
179
+ dtg_state = dtg_data.get("pre_migration_state", {})
180
+ time_zone = dtg_state.get("timezone")
181
+ dtg_name = dtg_state.get("datetime_group_name")
182
+
183
+ # Resolve address from CUCM Location entity
184
+ # The cucm_loc_id is like "cucm_location:SomeName" — may or may not be in the store
185
+ address = LocationAddress(country=self.default_country)
186
+ cucm_loc_data = store.get_object(cucm_loc_id)
187
+ if cucm_loc_data:
188
+ loc_state = cucm_loc_data.get("pre_migration_state", {})
189
+ address = LocationAddress(
190
+ address1=loc_state.get("address1"),
191
+ city=loc_state.get("city"),
192
+ state=loc_state.get("state"),
193
+ postal_code=loc_state.get("postal_code"),
194
+ country=loc_state.get("country") or self.default_country,
195
+ )
196
+
197
+ # Canonical ID: single-source if one pool, consolidated hash if multiple
198
+ # (from 03b-transform-mappers.md §13, Canonical ID Convention)
199
+ if len(dp_list) == 1:
200
+ canonical_id = f"location:{primary_dp['canonical_id'].replace('device_pool:', '')}"
201
+ else:
202
+ sorted_ids = sorted(dp["canonical_id"] for dp in dp_list)
203
+ hash_input = "|".join(sorted_ids)
204
+ hash_val = hashlib.sha256(hash_input.encode()).hexdigest()[:12]
205
+ canonical_id = f"location:{hash_val}"
206
+
207
+ # announcementLanguage must be lowercase
208
+ # (from 03b-transform-mappers.md §1: "Must be lowercase (en_us not en_US)")
209
+ # (from docs/reference/location-call-settings-core.md)
210
+ announcement_language = self.default_language.lower()
211
+
212
+ location = CanonicalLocation(
213
+ canonical_id=canonical_id,
214
+ provenance=Provenance(
215
+ source_system="cucm",
216
+ source_id=primary_dp.get("provenance", {}).get("source_id", ""),
217
+ source_name=dp_names[0],
218
+ cluster=primary_dp.get("provenance", {}).get("cluster"),
219
+ extracted_at=datetime.now(timezone.utc),
220
+ ),
221
+ status=MigrationStatus.ANALYZED,
222
+ name=name,
223
+ time_zone=time_zone,
224
+ preferred_language=self.default_language.lower(),
225
+ announcement_language=announcement_language,
226
+ address=address,
227
+ routing_prefix=None, # Set from config if available
228
+ outside_dial_digit=str(self.outside_dial_digit) if self.outside_dial_digit is not None else None,
229
+ calling_enabled=True,
230
+ cucm_device_pool_names=dp_names,
231
+ cucm_datetime_group_name=dtg_name,
232
+ cucm_location_name=cucm_loc_id.replace("cucm_location:", ""),
233
+ )
234
+
235
+ return location, warnings
@@ -0,0 +1,114 @@
1
+ """MOH mapper: CUCM MOH Audio Sources -> Webex per-location Music On Hold.
2
+
3
+ Maps CUCM Music On Hold audio sources to Webex Calling per-location MOH
4
+ settings (CanonicalMusicOnHold).
5
+
6
+ Default MOH sources map automatically. Custom audio sources produce
7
+ AUDIO_ASSET_MANUAL decisions because the WAV file must be manually
8
+ downloaded from CUCM and uploaded to Webex.
9
+
10
+ (from tier2-enterprise-expansion.md §2.3)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any
17
+
18
+ from wxcli.migration.models import (
19
+ CanonicalMusicOnHold,
20
+ DecisionType,
21
+ MapperResult,
22
+ MigrationStatus,
23
+ )
24
+ from wxcli.migration.store import MigrationStore
25
+ from wxcli.migration.transform.mappers.base import (
26
+ Mapper,
27
+ accept_option,
28
+ extract_provenance,
29
+ skip_option,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class MOHMapper(Mapper):
36
+ """Map CUCM MOH audio sources to Webex per-location MOH settings.
37
+
38
+ For each moh_source in the store:
39
+ - Default sources: create CanonicalMusicOnHold, no decision needed
40
+ - Custom sources: create CanonicalMusicOnHold + AUDIO_ASSET_MANUAL decision
41
+
42
+ (from tier2-enterprise-expansion.md §2.3)
43
+ """
44
+
45
+ name = "moh_mapper"
46
+ depends_on = ["location_mapper"]
47
+
48
+ def map(self, store: MigrationStore) -> MapperResult:
49
+ """Read CUCM MOH sources and produce Webex MOH settings."""
50
+ result = MapperResult()
51
+
52
+ for moh_data in store.get_objects("moh_source"):
53
+ canonical_id = moh_data["canonical_id"]
54
+ state = moh_data.get("pre_migration_state") or {}
55
+
56
+ name = state.get("name") or ""
57
+ source_file_name = state.get("source_file_name") or ""
58
+ is_default = state.get("is_default", False)
59
+ source_id = state.get("source_id") or ""
60
+
61
+ prov = extract_provenance(moh_data)
62
+
63
+ moh_obj = CanonicalMusicOnHold(
64
+ canonical_id=f"music_on_hold:{name}",
65
+ provenance=prov,
66
+ status=MigrationStatus.ANALYZED,
67
+ source_name=name,
68
+ source_file_name=source_file_name,
69
+ is_default=is_default,
70
+ cucm_source_id=source_id,
71
+ )
72
+ store.upsert_object(moh_obj)
73
+ result.objects_created += 1
74
+
75
+ # Custom audio sources need manual intervention
76
+ if not is_default:
77
+ decision = self._create_decision(
78
+ store=store,
79
+ decision_type=DecisionType.AUDIO_ASSET_MANUAL,
80
+ severity="MEDIUM",
81
+ summary=(
82
+ f"Custom MOH audio source '{name}' "
83
+ f"(file: {source_file_name or 'unknown'}) "
84
+ f"requires manual download from CUCM and upload to Webex"
85
+ ),
86
+ context={
87
+ "moh_source_name": name,
88
+ "source_file_name": source_file_name,
89
+ "source_id": source_id,
90
+ "canonical_id": f"music_on_hold:{name}",
91
+ },
92
+ options=[
93
+ accept_option(
94
+ "Admin downloads WAV from CUCM and uploads to Webex location MOH"
95
+ ),
96
+ _use_default_option(),
97
+ skip_option("Skip MOH migration for this source"),
98
+ ],
99
+ affected_objects=[f"music_on_hold:{name}"],
100
+ )
101
+ result.decisions.append(decision)
102
+
103
+ return result
104
+
105
+
106
+ def _use_default_option():
107
+ """Create a 'Use default MOH' decision option."""
108
+ from wxcli.migration.models import DecisionOption
109
+
110
+ return DecisionOption(
111
+ id="use_default",
112
+ label="Use Webex default",
113
+ impact="Accept Webex default MOH instead of custom audio",
114
+ )