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,74 @@
1
+ # cucm/ — CUCM Extraction Layer (Phase 03)
2
+
3
+ Connects to CUCM via AXL SOAP and extracts raw configuration into the SQLite store. Output is `DiscoveryResult.raw_data` — a dict of dicts of lists that feeds directly into `transform/pipeline.py:normalize_discovery()`.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |------|---------|
9
+ | `connection.py` | `AXLConnection` — zeep SOAP client with certificate handling and retry |
10
+ | `discovery.py` | `run_discovery(store, config)` — orchestrates all extractors in order, writes results to store |
11
+ | `unity_connection.py` | `UnityConnectionClient` — REST client for Unity Connection per-user VM settings |
12
+ | `extractors/base.py` | `BaseExtractor` ABC + `ExtractionResult` dataclass |
13
+ | `extractors/devices.py` | Phones (SEP) and their line appearances |
14
+ | `extractors/features.py` | Hunt pilots/lists/groups, call park, pickup groups, CTI route points, paging groups |
15
+ | `extractors/locations.py` | CUCM locations, device pools, datetime groups |
16
+ | `extractors/routing.py` | Gateways, SIP trunks, route groups, route lists, route patterns, CSS, partitions |
17
+ | `extractors/shared_lines.py` | Shared line detection (post-normalization, not an AXL extractor) |
18
+ | `extractors/templates.py` | Button templates + softkey templates (via SQL — AXL doesn't expose softkey template list) |
19
+ | `extractors/users.py` | EndUsers, lines (DNs), voicemail profiles |
20
+ | `extractors/voicemail.py` | Unity Connection voicemail pilot, voicemail profiles |
21
+ | `extractors/helpers.py` | `to_list()`, zeep response normalization helpers |
22
+ | `extractors/workspaces.py` | Common-area phone workspace classification (post-normalization) |
23
+ | `extractors/informational.py` | Tier 3: 20 informational object types (regions, SRST, MRG, app users, etc.) — report only |
24
+ | `extractors/tier4.py` | Tier 4 feature gaps: recording profiles, remote destinations, transformation patterns, EM device profiles |
25
+ | `extractors/remote_destinations.py` | Remote destinations for SNR |
26
+ | `extractors/e911.py` | ELIN groups, geo locations |
27
+ | `extractors/device_profiles.py` | Extension Mobility device profiles |
28
+ | `extractors/moh.py` | Music on Hold sources |
29
+ | `extractors/announcements.py` | Announcements / media resources |
30
+
31
+ ## Extraction Order
32
+
33
+ ```
34
+ locations → users → devices → features → routing → templates → voicemail → informational → tier4 → remote_destinations → e911 → device_profiles → moh → announcements
35
+ ```
36
+
37
+ Extractors are independent at extraction time — the order is a documentation convention, not a dependency constraint. All raw dicts go into `raw_data` keyed by extractor name.
38
+
39
+ ## raw_data Structure
40
+
41
+ `raw_data` is a `dict[str, dict[str, list]]`:
42
+ - Top key = extractor group (`"devices"`, `"users"`, `"features"`, etc.)
43
+ - Inner key = object type (`"phones"`, `"users"`, `"button_templates"`, etc.)
44
+ - Value = list of raw CUCM dicts (as returned by zeep, pre-normalized by `to_list()`)
45
+
46
+ `normalize_discovery()` in `transform/pipeline.py` consumes `raw_data` via `RAW_DATA_MAPPING`.
47
+
48
+ **Executive/assistant data** is extracted via SQL (no AXL list/get operations exist):
49
+ - `raw_data["features"]["executive_assistant_pairs"]` — pair dicts from `executiveassistant` table join
50
+ - `raw_data["features"]["executive_settings"]` — user service subscription dicts from `endusersubscribedservice` join
51
+
52
+ **Intercept candidates** are extracted via SQL heuristics:
53
+ - `raw_data["tier4"]["intercept_candidates"]` — heuristically-detected intercept-like configurations (blocked partition DNs, CFA-to-voicemail with no registered device). Uses two SQL queries for partition name pattern matching and call forwarding state detection.
54
+
55
+ **Standalone extractors** (wired into discovery.py alongside the 9 original extractors):
56
+ - `raw_data["remote_destinations"]["remote_destinations"]` — SNR remote destinations
57
+ - `raw_data["e911"]["elin_groups"]` — ELIN groups for E911
58
+ - `raw_data["e911"]["geo_locations"]` — geo locations for E911
59
+ - `raw_data["device_profiles"]["device_profiles"]` — Extension Mobility device profiles
60
+ - `raw_data["moh"]["moh_sources"]` — Music on Hold audio sources
61
+ - `raw_data["announcements"]["announcements"]` — announcement / media resources
62
+
63
+ ## Key Gotchas
64
+
65
+ - **Softkey templates require SQL.** `addSoftkeyTemplate`/`listSoftkeyTemplate` don't exist in AXL v15.0. The `TemplateExtractor` uses direct SQL (`executeSQLQuery`) to fetch softkey template data. See `extractors/templates.py`.
66
+ - **Button templates use `<buttonNumber>` not `<index>`** in `addPhoneButtonTemplate`. The extractor normalizes this on the way out.
67
+ - **BLF name mismatch.** The feature is called "Speed Dial BLF" in `addPhone` but appears as "Busy Lamp Field" in `getPhone` responses. Extractors handle both.
68
+ - **Raw phones must be preserved.** `normalize_phone()` creates a `CanonicalDevice` but loses the raw dict. `pipeline.py` re-stores each phone as `MigrationObject(canonical_id="phone:{name}", pre_migration_state=phone)` so mappers can read `store.get_objects("phone")` and access `speeddials`, `busyLampFields`, and per-line call forwarding.
69
+ - **Unity Connection is optional.** If no Unity Connection config is present, `unity_connection.py` is skipped and `raw_data["voicemail"]["unity_user_settings"]` is absent.
70
+ - **Base station MACs require Cisco Bifrost.** DECT base station MAC validation requires the Cisco manufacturing database — fake MACs fail provisioning. Not an extraction issue but affects test beds.
71
+ - **CTI Route Points use protocol "CTI Route Point"** in AXL — different from standard phone protocols. Use that exact string in `addCTIRoutePoint`.
72
+ - **Pickup group members via `updateLine`**, not `addCallPickupGroup`. Creating a pickup group with `<members>` fails on CUCM 15.0 (null priority FK constraint). The extractor reads the existing members; provisioning test bed uses `updateLine` pattern.
73
+ - **`getPhone` now extracts device-settings fields.** `commonPhoneConfigName`, `productSpecificConfiguration`, `userLocale`, `networkLocale`, `dndOption`, `dndStatus`, and `enableExtensionMobility` are all in `PHONE_GET_RETURNED_TAGS` for the DeviceSettingsMapper.
74
+ - **InformationalExtractor fetches CPC details.** After listing Common Phone Configs, the extractor calls `getCommonPhoneConfig` per item to retrieve `vendorConfig` (model-specific XML settings).
@@ -0,0 +1,5 @@
1
+ """CUCM extraction layer for the migration pipeline.
2
+
3
+ Connects to CUCM via AXL SOAP (zeep) and optionally to Unity Connection
4
+ via CUPI REST to extract configuration data for migration analysis.
5
+ """
@@ -0,0 +1,265 @@
1
+ """AXL SOAP client via zeep.
2
+
3
+ Provides the AXL connection, version detection, and pagination helper
4
+ that all extractors use.
5
+
6
+ Sources:
7
+ - cucm-wxc-migration.md lines 301-309 (WSDL, auth, pagination)
8
+ - 02b-cucm-extraction.md §1 (connection setup, pagination strategy)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import urllib3
15
+ from collections import OrderedDict
16
+ from typing import Any
17
+
18
+ from requests import Session
19
+ from requests.auth import HTTPBasicAuth
20
+ from zeep import Client, Settings
21
+ from zeep.helpers import serialize_object
22
+ from zeep.transports import Transport
23
+
24
+ # CUCM uses self-signed certs — suppress the noise
25
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class AXLConnectionError(Exception):
31
+ """Raised when the AXL connection cannot be established."""
32
+
33
+
34
+ class AXLConnection:
35
+ """AXL SOAP client using zeep.
36
+
37
+ (from 02b §1: connection setup)
38
+
39
+ Args:
40
+ host: CUCM hostname or IP address.
41
+ username: AXL admin username.
42
+ password: AXL admin password.
43
+ version: CUCM AXL schema version (default "14.0").
44
+ verify_ssl: Whether to verify SSL certificates (default False —
45
+ CUCM typically uses self-signed certs).
46
+ timeout: SOAP request timeout in seconds (default 30).
47
+ wsdl_path: Path to a local AXL WSDL file. If provided, zeep loads
48
+ the schema locally instead of fetching from the CUCM server.
49
+ Download from CUCM Admin > Application > Plugins > "Cisco AXL Toolkit".
50
+ port: CUCM AXL port (default 8443).
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ host: str,
56
+ username: str,
57
+ password: str,
58
+ version: str = "14.0",
59
+ verify_ssl: bool = False,
60
+ timeout: int = 30,
61
+ wsdl_path: str | None = None,
62
+ port: int = 8443,
63
+ ) -> None:
64
+ self.host = host
65
+ self.port = port
66
+ self.version = version
67
+
68
+ session = Session()
69
+ session.auth = HTTPBasicAuth(username, password)
70
+ session.verify = verify_ssl
71
+ transport = Transport(session=session, timeout=timeout)
72
+
73
+ # Use local WSDL if provided, otherwise fetch from CUCM
74
+ if wsdl_path:
75
+ self.wsdl_url = wsdl_path
76
+ else:
77
+ self.wsdl_url = f"https://{host}:{self.port}/axl/AXLAPIService?wsdl"
78
+
79
+ # strict=False: CUCM responses may contain elements not in the WSDL
80
+ # (e.g. isAnonymous in getSipTrunk on CUCM 15.0). Without this,
81
+ # zeep raises XMLSyntaxError for any unexpected element.
82
+ settings = Settings(strict=False, xml_huge_tree=True)
83
+
84
+ try:
85
+ self.client = Client(
86
+ self.wsdl_url, transport=transport, settings=settings,
87
+ )
88
+ except Exception as exc:
89
+ raise AXLConnectionError(
90
+ f"Failed to load WSDL from {self.wsdl_url}: {exc}"
91
+ ) from exc
92
+
93
+ # When using a local WSDL, zeep needs the service address pointed at the real CUCM
94
+ service_url = f"https://{host}:{self.port}/axl/"
95
+ try:
96
+ self.service = self.client.create_service(
97
+ "{http://www.cisco.com/AXLAPIService/}AXLAPIBinding",
98
+ service_url,
99
+ )
100
+ except Exception as exc:
101
+ raise AXLConnectionError(
102
+ f"Failed to initialize AXL service at {service_url}: {exc}"
103
+ ) from exc
104
+
105
+ def get_version(self) -> str:
106
+ """Detect CUCM version via getCCMVersion().
107
+
108
+ (from cucm-wxc-migration.md line 309)
109
+ """
110
+ response = self._serialize(self.service.getCCMVersion())
111
+ version_info = response.get("return", {})
112
+ if isinstance(version_info, dict):
113
+ comp = version_info.get("componentVersion", {})
114
+ if isinstance(comp, dict):
115
+ return comp.get("version", self.version)
116
+ return str(comp) if comp else self.version
117
+ return self.version
118
+
119
+ # ------------------------------------------------------------------
120
+ # Pagination helper
121
+ # ------------------------------------------------------------------
122
+
123
+ def paginated_list(
124
+ self,
125
+ method_name: str,
126
+ search_criteria: dict[str, str],
127
+ returned_tags: dict[str, str],
128
+ page_size: int = 200,
129
+ ) -> list[dict[str, Any]]:
130
+ """Generic paginated AXL list operation.
131
+
132
+ (from 02b §1: pagination strategy)
133
+
134
+ Args:
135
+ method_name: AXL method (e.g. 'listEndUser').
136
+ search_criteria: Filter dict (e.g. {'firstName': '%'} for all).
137
+ returned_tags: Fields to return (AXL only returns requested fields).
138
+ page_size: Rows per page (default 200, AXL max for most methods).
139
+ """
140
+ all_results: list[dict[str, Any]] = []
141
+ skip = 0
142
+ while True:
143
+ raw = getattr(self.service, method_name)(
144
+ searchCriteria=search_criteria,
145
+ returnedTags=returned_tags,
146
+ first=str(page_size),
147
+ skip=str(skip),
148
+ )
149
+ response = self._serialize(raw)
150
+ batch = self._extract_rows(response)
151
+ if not batch:
152
+ break
153
+ all_results.extend(batch)
154
+ if len(batch) < page_size:
155
+ break # Last page
156
+ skip += page_size
157
+ return all_results
158
+
159
+ def get_detail(
160
+ self, method_name: str, **kwargs: Any
161
+ ) -> dict[str, Any] | None:
162
+ """Single-object get operation (e.g. getPhone by name or UUID).
163
+
164
+ Returns the inner result dict, or None on error.
165
+ """
166
+ try:
167
+ raw = getattr(self.service, method_name)(**kwargs)
168
+ response = self._serialize(raw)
169
+ ret = response.get("return", {})
170
+ # get responses wrap the object under a single key
171
+ if isinstance(ret, dict):
172
+ for key, value in ret.items():
173
+ if key != "_value_1" and value is not None:
174
+ return self._serialize(value)
175
+ return ret
176
+ except Exception as exc:
177
+ logger.warning("[%s] %s", method_name, exc)
178
+ return None
179
+
180
+ def execute_sql(self, query: str) -> list[dict[str, Any]]:
181
+ """Run an executeSQLQuery against CUCM's informix database.
182
+
183
+ (from 02b §5: performance considerations for large clusters)
184
+
185
+ Returns list of row dicts. Each row is a dict mapping column name → value.
186
+ """
187
+ raw = self.service.executeSQLQuery(sql=query)
188
+ response = self._serialize(raw)
189
+ rows = self._extract_rows(response)
190
+ # SQL rows may come back as lxml Elements (when using WSDL + zeep).
191
+ # Convert each row to a plain dict.
192
+ result: list[dict[str, Any]] = []
193
+ for row in rows:
194
+ if isinstance(row, dict):
195
+ result.append(row)
196
+ elif isinstance(row, list):
197
+ # List of lxml Elements: each has .tag and .text
198
+ row_dict: dict[str, Any] = {}
199
+ for elem in row:
200
+ tag = getattr(elem, "tag", None)
201
+ if tag:
202
+ # Strip namespace if present
203
+ tag = tag.split("}")[-1] if "}" in tag else tag
204
+ text = getattr(elem, "text", None)
205
+ if tag:
206
+ row_dict[tag] = text
207
+ result.append(row_dict)
208
+ elif hasattr(row, "__dict__"):
209
+ result.append(dict(row.__dict__))
210
+ else:
211
+ result.append({"_raw": str(row)})
212
+ return result
213
+
214
+ # ------------------------------------------------------------------
215
+ # Internal helpers
216
+ # ------------------------------------------------------------------
217
+
218
+ @staticmethod
219
+ def _serialize(obj: Any) -> dict[str, Any]:
220
+ """Convert zeep typed objects to plain Python dicts.
221
+
222
+ Zeep with a local WSDL returns typed objects (e.g. GetPhoneRes)
223
+ instead of plain dicts. ``serialize_object`` converts them to
224
+ OrderedDicts recursively. We convert to regular dicts for consistency.
225
+ """
226
+ if obj is None:
227
+ return {}
228
+ if isinstance(obj, dict):
229
+ return obj
230
+ serialized = serialize_object(obj, dict)
231
+ if isinstance(serialized, dict):
232
+ return serialized
233
+ return {"_raw": serialized}
234
+
235
+ @staticmethod
236
+ def _extract_rows(response: Any) -> list[dict[str, Any]]:
237
+ """Extract the row list from an AXL list/SQL response.
238
+
239
+ AXL responses: {'return': {'{objectType}': [rows...]}}
240
+ Zeep converts this to a dict with one key matching the object type.
241
+ (from 02b §3: base extractor _extract_rows)
242
+ """
243
+ if response is None:
244
+ return []
245
+ # Extract the 'return' value from the response.
246
+ # Zeep response objects support dict-like access.
247
+ ret: Any = None
248
+ if hasattr(response, "get"):
249
+ ret = response.get("return")
250
+ elif hasattr(response, "__getitem__"):
251
+ try:
252
+ ret = response["return"]
253
+ except (KeyError, TypeError):
254
+ return []
255
+ else:
256
+ return []
257
+ if not ret:
258
+ return []
259
+ if isinstance(ret, dict):
260
+ for key, value in ret.items():
261
+ if key != "_value_1" and value is not None:
262
+ if isinstance(value, list):
263
+ return value
264
+ return [value]
265
+ return []
@@ -0,0 +1,225 @@
1
+ """Discovery orchestrator — runs all extractors and writes results to store.
2
+
3
+ Entry point for the extraction phase. Connects to CUCM via AXL SOAP,
4
+ runs each extractor in sequence, and writes raw CUCM dicts into the SQLite
5
+ store for pass 1 normalization.
6
+
7
+ Sources:
8
+ - 02b-cucm-extraction.md §1 (orchestration, extractor order, summary)
9
+ - 02-normalization-architecture.md (two-pass ELT pattern)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
+
19
+ from rich.console import Console
20
+
21
+ from wxcli.migration.cucm.connection import AXLConnection
22
+ from wxcli.migration.cucm.extractors.base import ExtractionResult
23
+ from wxcli.migration.cucm.extractors.devices import DeviceExtractor
24
+ from wxcli.migration.cucm.extractors.features import FeatureExtractor
25
+ from wxcli.migration.cucm.extractors.locations import LocationExtractor
26
+ from wxcli.migration.cucm.extractors.routing import RoutingExtractor
27
+ from wxcli.migration.cucm.extractors.users import UserExtractor
28
+ from wxcli.migration.cucm.extractors.templates import TemplateExtractor
29
+ from wxcli.migration.cucm.extractors.voicemail import VoicemailExtractor
30
+ from wxcli.migration.cucm.extractors.informational import InformationalExtractor
31
+ from wxcli.migration.cucm.extractors.tier4 import Tier4Extractor
32
+ from wxcli.migration.cucm.extractors.remote_destinations import RemoteDestinationExtractor
33
+ from wxcli.migration.cucm.extractors.e911 import E911Extractor
34
+ from wxcli.migration.cucm.extractors.device_profiles import DeviceProfileExtractor
35
+ from wxcli.migration.cucm.extractors.moh import MOHExtractor
36
+ from wxcli.migration.cucm.extractors.announcements import AnnouncementExtractor
37
+ from wxcli.migration.cucm.unity_connection import UnityConnectionClient
38
+ from wxcli.migration.store import MigrationStore
39
+
40
+ logger = logging.getLogger(__name__)
41
+ console = Console()
42
+
43
+ # Extraction order (from 02b §1).
44
+ # Extractors are independent at the extraction phase — order is flexible.
45
+ # shared_lines and workspaces are post-processing steps, not AXL extractors,
46
+ # and run during normalization pass 2.
47
+ EXTRACTOR_ORDER = [
48
+ "locations", # Device pools, datetime groups, CUCM locations — no dependencies
49
+ "users", # EndUsers — no extraction-phase dependencies
50
+ "devices", # Phones — no extraction-phase dependencies
51
+ "routing", # Partitions, CSS, route patterns, gateways, trunks — no dependencies
52
+ "features", # Hunt pilots, CTI RPs, call park, pickup, schedules — no dependencies
53
+ "voicemail", # Voicemail profiles, pilot numbers — no dependencies
54
+ "templates", # Button templates, softkey templates — no dependencies
55
+ "informational", # Tier 3: report-only informational objects — no dependencies
56
+ "tier4", # Tier 4 feature gaps: recording, SNR, transformations, EM profiles
57
+ "remote_destinations", # SNR remote destinations
58
+ "e911", # ELIN groups, geo locations
59
+ "device_profiles", # EM device profiles
60
+ "moh", # Music on Hold sources
61
+ "announcements", # Announcements / media resources
62
+ ]
63
+
64
+
65
+ class DiscoveryResult:
66
+ """Aggregate result of a full discovery run.
67
+
68
+ (from 02b §1: discovery run summary)
69
+
70
+ **Data flow contract for Phase 04 normalizers:**
71
+
72
+ Discovery does NOT write raw data to the SQLite store. Raw CUCM dicts
73
+ live in ``raw_data`` (in-memory). Phase 04 normalizers consume
74
+ ``raw_data``, transform to canonical models, and write to the store
75
+ via ``MigrationStore.upsert_object()``.
76
+
77
+ **raw_data structure per extractor:**
78
+
79
+ - ``raw_data["locations"]``: ``device_pools``, ``datetime_groups``, ``cucm_locations``
80
+ - ``raw_data["users"]``: ``users``
81
+ - ``raw_data["devices"]``: ``phones`` (lines pre-filtered: speed dials removed)
82
+ - ``raw_data["routing"]``: ``partitions``, ``css_list``, ``route_patterns``,
83
+ ``gateways``, ``sip_trunks``, ``route_groups``, ``route_lists``,
84
+ ``translation_patterns``
85
+ - ``raw_data["features"]``: ``hunt_pilots``, ``hunt_lists``, ``line_groups``,
86
+ ``cti_route_points``, ``call_parks``, ``pickup_groups``, ``time_schedules``,
87
+ ``time_periods``
88
+ - ``raw_data["voicemail"]``: ``voicemail_profiles``, ``voicemail_pilots``,
89
+ and optionally ``unity_user_settings`` (dict keyed by CUCM userid)
90
+ """
91
+
92
+ def __init__(self) -> None:
93
+ self.run_id: str = ""
94
+ self.cucm_version: str = ""
95
+ self.started_at: str = ""
96
+ self.completed_at: str = ""
97
+ self.extractor_results: dict[str, ExtractionResult] = {}
98
+ self.raw_data: dict[str, dict[str, Any]] = {}
99
+
100
+ @property
101
+ def total_objects(self) -> int:
102
+ return sum(r.total for r in self.extractor_results.values())
103
+
104
+ @property
105
+ def total_failed(self) -> int:
106
+ return sum(r.failed for r in self.extractor_results.values())
107
+
108
+ def to_summary(self) -> dict[str, Any]:
109
+ """Produce the journal summary dict (from 02b §1)."""
110
+ return {
111
+ "run_type": "discovery",
112
+ "run_id": self.run_id,
113
+ "started_at": self.started_at,
114
+ "completed_at": self.completed_at,
115
+ "cucm_version": self.cucm_version,
116
+ "extractors": {
117
+ name: {"total": r.total, "failed": r.failed}
118
+ for name, r in self.extractor_results.items()
119
+ },
120
+ }
121
+
122
+
123
+ def run_discovery(
124
+ connection: AXLConnection,
125
+ store: MigrationStore,
126
+ unity_client: UnityConnectionClient | None = None,
127
+ ) -> DiscoveryResult:
128
+ """Run full CUCM discovery — extract all object types.
129
+
130
+ (from 02b §1: discovery orchestration)
131
+
132
+ Args:
133
+ connection: Established AXL SOAP connection.
134
+ store: Migration store for writing results and journal entries.
135
+ unity_client: Optional Unity Connection client for voicemail detail.
136
+
137
+ Returns:
138
+ DiscoveryResult with per-extractor counts, errors, and raw data.
139
+ """
140
+ result = DiscoveryResult()
141
+ result.run_id = store.current_run_id
142
+ result.started_at = datetime.now(timezone.utc).isoformat()
143
+
144
+ # Detect CUCM version (from cucm-wxc-migration.md line 309)
145
+ try:
146
+ result.cucm_version = connection.get_version()
147
+ logger.info("CUCM version: %s", result.cucm_version)
148
+ except Exception as exc:
149
+ logger.warning("Could not detect CUCM version: %s", exc)
150
+ result.cucm_version = "unknown"
151
+
152
+ # Build extractors in order (from 02b §1: EXTRACTOR_ORDER)
153
+ extractors: dict[str, Any] = {
154
+ "locations": LocationExtractor(connection),
155
+ "users": UserExtractor(connection),
156
+ "devices": DeviceExtractor(connection),
157
+ "routing": RoutingExtractor(connection),
158
+ "features": FeatureExtractor(connection),
159
+ "voicemail": VoicemailExtractor(connection, unity_client=unity_client),
160
+ "templates": TemplateExtractor(connection),
161
+ "informational": InformationalExtractor(connection),
162
+ "tier4": Tier4Extractor(connection),
163
+ "remote_destinations": RemoteDestinationExtractor(connection),
164
+ "e911": E911Extractor(connection),
165
+ "device_profiles": DeviceProfileExtractor(connection),
166
+ "moh": MOHExtractor(connection),
167
+ "announcements": AnnouncementExtractor(connection),
168
+ }
169
+
170
+ # Run each extractor in order
171
+ for name in EXTRACTOR_ORDER:
172
+ extractor = extractors[name]
173
+ logger.info("--- Running extractor: %s ---", name)
174
+ console.print(f" Extracting {name}...")
175
+ try:
176
+ ext_result = extractor.extract()
177
+ except Exception as exc:
178
+ logger.error("[%s] Extraction failed: %s", name, exc)
179
+ ext_result = ExtractionResult(extractor=name)
180
+ ext_result.errors.append(f"Unrecoverable error: {exc}")
181
+
182
+ result.extractor_results[name] = ext_result
183
+ result.raw_data[name] = extractor.results
184
+ console.print(f" {name}: {ext_result.total} objects")
185
+ if ext_result.errors:
186
+ for err in ext_result.errors:
187
+ console.print(f" [yellow]{name}: {err}[/yellow]")
188
+
189
+ # Extract Unity Connection per-user voicemail settings if client available
190
+ if unity_client is not None:
191
+ user_extractor = extractors["users"]
192
+ user_dicts = user_extractor.results.get("users", [])
193
+ user_ids = [u.get("userid", "") for u in user_dicts if u.get("userid")]
194
+ vm_extractor = extractors["voicemail"]
195
+ unity_settings = vm_extractor.extract_unity_settings(user_ids)
196
+ # Store Unity settings alongside AXL voicemail data
197
+ result.raw_data.setdefault("voicemail", {})["unity_user_settings"] = unity_settings
198
+
199
+ result.completed_at = datetime.now(timezone.utc).isoformat()
200
+
201
+ # Write summary to journal (from 02b §1: discovery run summary)
202
+ # Ensure the system sentinel object exists so the FK constraint is satisfied
203
+ store.conn.execute(
204
+ """INSERT OR IGNORE INTO objects
205
+ (canonical_id, object_type, status, data, created_at, updated_at)
206
+ VALUES (?, ?, ?, ?, ?, ?)""",
207
+ ("system:discovery", "system", "discovered", "{}", result.completed_at, result.completed_at),
208
+ )
209
+ store.conn.commit()
210
+ summary = result.to_summary()
211
+ store.add_journal_entry(
212
+ entry_type="discovery_complete",
213
+ canonical_id="system:discovery",
214
+ resource_type="discovery",
215
+ request=summary,
216
+ )
217
+
218
+ logger.info(
219
+ "Discovery complete: %d objects extracted (%d failed) across %d extractors",
220
+ result.total_objects,
221
+ result.total_failed,
222
+ len(result.extractor_results),
223
+ )
224
+
225
+ return result
@@ -0,0 +1 @@
1
+ """CUCM AXL extractors — one module per object domain."""
@@ -0,0 +1,92 @@
1
+ """Announcement extractor for CUCM AXL.
2
+
3
+ Pulls Announcements from CUCM AXL for custom audio migration advisory.
4
+
5
+ Sources:
6
+ - tier2-enterprise-expansion.md §2.4 (announcement extraction)
7
+ - AXL WSDL: listAnnouncement / getAnnouncement
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # returnedTags for listAnnouncement
20
+ ANNOUNCEMENT_LIST_RETURNED_TAGS = {
21
+ "name": "",
22
+ "description": "",
23
+ }
24
+
25
+ # Additional fields from getAnnouncement
26
+ ANNOUNCEMENT_GET_RETURNED_TAGS = {
27
+ "name": "",
28
+ "description": "",
29
+ "announcementFile": "",
30
+ }
31
+
32
+
33
+ class AnnouncementExtractor(BaseExtractor):
34
+ """Extract Announcements from CUCM AXL.
35
+
36
+ Announcements are custom audio files uploaded to CUCM and referenced
37
+ by Auto Attendants, Call Queues, MOH sources, and other features.
38
+
39
+ (from tier2-enterprise-expansion.md §2.4)
40
+ """
41
+
42
+ name = "announcements"
43
+
44
+ def __init__(self, connection) -> None:
45
+ super().__init__(connection)
46
+ self.results: dict[str, list[dict[str, Any]]] = {}
47
+
48
+ def extract(self) -> ExtractionResult:
49
+ result = ExtractionResult(extractor=self.name)
50
+ self.results["announcements"] = self._extract_announcements(result)
51
+ return result
52
+
53
+ def _extract_announcements(
54
+ self, result: ExtractionResult
55
+ ) -> list[dict[str, Any]]:
56
+ """List all announcements, then get full detail for each."""
57
+ announcements: list[dict[str, Any]] = []
58
+ try:
59
+ summaries = self.paginated_list(
60
+ "listAnnouncement",
61
+ {"name": "%"},
62
+ ANNOUNCEMENT_LIST_RETURNED_TAGS,
63
+ )
64
+ except Exception as exc:
65
+ msg = f"listAnnouncement failed: {exc}"
66
+ logger.error("[%s] %s", self.name, msg)
67
+ result.errors.append(msg)
68
+ return announcements
69
+
70
+ for summary in summaries:
71
+ result.total += 1
72
+ ann_name = summary.get("name", "")
73
+ if not ann_name:
74
+ result.failed += 1
75
+ continue
76
+ try:
77
+ detail = self.get_detail("getAnnouncement", name=ann_name)
78
+ except Exception as exc:
79
+ logger.warning(
80
+ "[%s] getAnnouncement error for %s: %s",
81
+ self.name, ann_name, exc,
82
+ )
83
+ announcements.append(summary)
84
+ continue
85
+ if detail is None:
86
+ announcements.append(summary)
87
+ continue
88
+ # Merge list-level fields into detail
89
+ merged = {**summary, **detail}
90
+ announcements.append(merged)
91
+
92
+ return announcements