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,309 @@
1
+ """Feature extractors for CUCM AXL.
2
+
3
+ Pulls Hunt Pilots, Hunt Lists, Line Groups, CTI Route Points, Call Park,
4
+ Pickup Groups, Time Schedules, and Time Periods.
5
+
6
+ Sources:
7
+ - 02b-cucm-extraction.md §2.5 (feature extraction)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import Any
14
+
15
+ from wxcli.migration.cucm.connection import AXLConnection
16
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
17
+ from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # AXL Gotchas (discovered test bed expansion 2026-03-24):
22
+ # - addCallPickupGroup with <members>/<directoryNumber> fails on CUCM 15.0
23
+ # (null priority FK constraint). Use empty create + updateLine workaround.
24
+ # - No listPagingGroup/getPagingGroup AXL methods exist. Paging requires
25
+ # InformaCast/Cisco Paging Server. CanonicalPagingGroup is manual-only.
26
+ # - CTI Route Points may require SCCP instead of SIP on some device pools.
27
+ # - TimePeriod monthOfYear: 3-letter abbreviations only (Dec, Jan, etc.)
28
+
29
+ # ------------------------------------------------------------------
30
+ # ReturnedTags constants
31
+ # ------------------------------------------------------------------
32
+
33
+ # Verified: listHuntPilot accepts these fields per WSDL LHuntPilot schema.
34
+ # Fields like mohSourceId, maxCallersInQueue, huntTimerCallPick, overflowDestination,
35
+ # enabled are only on getHuntPilot.
36
+ HUNT_PILOT_LIST_RETURNED_TAGS = {
37
+ "pattern": "", "description": "", "routePartitionName": "",
38
+ "huntListName": "",
39
+ }
40
+
41
+ # Verified: list operations do NOT accept nested fields (members, lines).
42
+ # Use list tags for discovery, get operations for full detail.
43
+ HUNT_LIST_LIST_RETURNED_TAGS = {
44
+ "name": "", "description": "",
45
+ }
46
+
47
+ LINE_GROUP_LIST_RETURNED_TAGS = {
48
+ "name": "", "distributionAlgorithm": "",
49
+ }
50
+
51
+ CTI_RP_LIST_RETURNED_TAGS = {
52
+ "name": "", "description": "", "devicePoolName": "", "callingSearchSpaceName": "",
53
+ }
54
+
55
+ CALL_PARK_RETURNED_TAGS = {
56
+ "pattern": "", "description": "", "routePartitionName": "",
57
+ }
58
+
59
+ PICKUP_GROUP_LIST_RETURNED_TAGS = {
60
+ "name": "",
61
+ }
62
+
63
+ TIME_SCHEDULE_LIST_RETURNED_TAGS = {
64
+ "name": "",
65
+ }
66
+
67
+ TIME_PERIOD_LIST_RETURNED_TAGS = {
68
+ "name": "",
69
+ }
70
+
71
+
72
+ class FeatureExtractor(BaseExtractor):
73
+ """Extract calling features from CUCM AXL.
74
+
75
+ Covers Hunt Pilots, Hunt Lists, Line Groups, CTI Route Points,
76
+ Call Park, Pickup Groups, Time Schedules, and Time Periods.
77
+
78
+ (from 02b §2.5)
79
+ """
80
+
81
+ name = "features"
82
+
83
+ def __init__(self, connection: AXLConnection) -> None:
84
+ super().__init__(connection)
85
+ self.results: dict[str, list[dict[str, Any]]] = {}
86
+
87
+ def extract(self) -> ExtractionResult:
88
+ """Run all feature extractions.
89
+
90
+ Returns an ExtractionResult summarizing total objects and errors.
91
+ """
92
+ result = ExtractionResult(extractor=self.name)
93
+
94
+ self.results["hunt_pilots"] = self._extract_hunt_pilots(result)
95
+ self.results["hunt_lists"] = self._extract_hunt_lists(result)
96
+ self.results["line_groups"] = self._extract_line_groups(result)
97
+ self.results["cti_route_points"] = self._extract_cti_route_points(result)
98
+ self.results["call_parks"] = self._extract_call_parks(result)
99
+ self.results["pickup_groups"] = self._extract_pickup_groups(result)
100
+ self.results["time_schedules"] = self._extract_time_schedules(result)
101
+ self.results["time_periods"] = self._extract_time_periods(result)
102
+ self.results["executive_assistant_pairs"] = self._extract_executive_assistant_pairs(result)
103
+ self.results["executive_settings"] = self._extract_executive_settings(result)
104
+
105
+ return result
106
+
107
+ # ------------------------------------------------------------------
108
+ # Common list+get pattern
109
+ # ------------------------------------------------------------------
110
+
111
+ def _list_and_get(
112
+ self,
113
+ result: ExtractionResult,
114
+ list_method: str,
115
+ get_method: str,
116
+ search_criteria: dict[str, str],
117
+ returned_tags: dict[str, str],
118
+ name_field: str = "name",
119
+ ) -> list[dict[str, Any]]:
120
+ """Common pattern: paginated list then per-object get.
121
+
122
+ On get failure (exception or None), the object is skipped and counted
123
+ as failed — consistent with DeviceExtractor and RoutingExtractor.
124
+ """
125
+ items: list[dict[str, Any]] = []
126
+ try:
127
+ summaries = self.paginated_list(list_method, search_criteria, returned_tags)
128
+ except Exception as exc:
129
+ msg = f"{list_method} failed: {exc}"
130
+ logger.error("[%s] %s", self.name, msg)
131
+ result.errors.append(msg)
132
+ return items
133
+
134
+ for summary in summaries:
135
+ result.total += 1
136
+ obj_name = ref_value(summary.get(name_field)) or summary.get(name_field, "")
137
+ if not obj_name:
138
+ result.failed += 1
139
+ result.errors.append(f"{list_method} returned object with no {name_field}")
140
+ continue
141
+
142
+ try:
143
+ kwargs = {name_field: obj_name}
144
+ # Hunt pilots are looked up by pattern + partition, not name
145
+ if name_field == "pattern":
146
+ partition = ref_value(summary.get("routePartitionName"))
147
+ kwargs["routePartitionName"] = partition or ""
148
+ detail = self.get_detail(get_method, **kwargs)
149
+ except Exception as exc:
150
+ result.failed += 1
151
+ msg = f"{get_method}({obj_name}): {exc}"
152
+ logger.warning("[%s] %s", self.name, msg)
153
+ result.errors.append(msg)
154
+ continue
155
+
156
+ if detail is None:
157
+ result.failed += 1
158
+ result.errors.append(f"{get_method} returned None for {obj_name}")
159
+ continue
160
+
161
+ items.append(detail)
162
+
163
+ return items
164
+
165
+ # ------------------------------------------------------------------
166
+ # Hunt Pilots
167
+ # ------------------------------------------------------------------
168
+
169
+ def _extract_hunt_pilots(self, result: ExtractionResult) -> list[dict[str, Any]]:
170
+ """Extract hunt pilots. Verified against live CUCM 15.0 2026-03-23."""
171
+ return self._list_and_get(
172
+ result, "listHuntPilot", "getHuntPilot",
173
+ {"pattern": "%"}, HUNT_PILOT_LIST_RETURNED_TAGS, name_field="pattern",
174
+ )
175
+
176
+ # ------------------------------------------------------------------
177
+ # Hunt Lists
178
+ # ------------------------------------------------------------------
179
+
180
+ def _extract_hunt_lists(self, result: ExtractionResult) -> list[dict[str, Any]]:
181
+ """Extract hunt lists. Verified via pipeline execution against CUCM 15.0 testbed."""
182
+ return self._list_and_get(
183
+ result, "listHuntList", "getHuntList",
184
+ {"name": "%"}, HUNT_LIST_LIST_RETURNED_TAGS,
185
+ )
186
+
187
+ # ------------------------------------------------------------------
188
+ # Line Groups
189
+ # ------------------------------------------------------------------
190
+
191
+ def _extract_line_groups(self, result: ExtractionResult) -> list[dict[str, Any]]:
192
+ """Extract line groups. Verified via pipeline execution against CUCM 15.0 testbed."""
193
+ return self._list_and_get(
194
+ result, "listLineGroup", "getLineGroup",
195
+ {"name": "%"}, LINE_GROUP_LIST_RETURNED_TAGS,
196
+ )
197
+
198
+ # ------------------------------------------------------------------
199
+ # CTI Route Points
200
+ # ------------------------------------------------------------------
201
+
202
+ def _extract_cti_route_points(self, result: ExtractionResult) -> list[dict[str, Any]]:
203
+ """Extract CTI route points. Verified via pipeline execution against CUCM 15.0 testbed."""
204
+ return self._list_and_get(
205
+ result, "listCtiRoutePoint", "getCtiRoutePoint",
206
+ {"name": "%"}, CTI_RP_LIST_RETURNED_TAGS,
207
+ )
208
+
209
+ # ------------------------------------------------------------------
210
+ # Call Parks — list only, no get needed
211
+ # ------------------------------------------------------------------
212
+
213
+ def _extract_call_parks(self, result: ExtractionResult) -> list[dict[str, Any]]:
214
+ """Extract call parks (list only, no get needed). Verified via pipeline execution."""
215
+ try:
216
+ parks = self.paginated_list(
217
+ "listCallPark", {"pattern": "%"}, CALL_PARK_RETURNED_TAGS,
218
+ )
219
+ result.total += len(parks)
220
+ return parks
221
+ except Exception as exc:
222
+ msg = f"listCallPark failed: {exc}"
223
+ logger.error("[%s] %s", self.name, msg)
224
+ result.errors.append(msg)
225
+ return []
226
+
227
+ # ------------------------------------------------------------------
228
+ # Pickup Groups
229
+ # ------------------------------------------------------------------
230
+
231
+ def _extract_pickup_groups(self, result: ExtractionResult) -> list[dict[str, Any]]:
232
+ """Extract pickup groups. Verified against live CUCM 15.0 2026-03-24."""
233
+ return self._list_and_get(
234
+ result, "listCallPickupGroup", "getCallPickupGroup",
235
+ {"pattern": "%"}, PICKUP_GROUP_LIST_RETURNED_TAGS,
236
+ )
237
+
238
+ # ------------------------------------------------------------------
239
+ # Time Schedules
240
+ # ------------------------------------------------------------------
241
+
242
+ def _extract_time_schedules(self, result: ExtractionResult) -> list[dict[str, Any]]:
243
+ """Extract time schedules. Verified via pipeline execution against CUCM 15.0 testbed."""
244
+ return self._list_and_get(
245
+ result, "listTimeSchedule", "getTimeSchedule",
246
+ {"name": "%"}, TIME_SCHEDULE_LIST_RETURNED_TAGS,
247
+ )
248
+
249
+ # ------------------------------------------------------------------
250
+ # Time Periods
251
+ # ------------------------------------------------------------------
252
+
253
+ def _extract_time_periods(self, result: ExtractionResult) -> list[dict[str, Any]]:
254
+ """Extract time periods. Verified via pipeline execution against CUCM 15.0 testbed."""
255
+ return self._list_and_get(
256
+ result, "listTimePeriod", "getTimePeriod",
257
+ {"name": "%"}, TIME_PERIOD_LIST_RETURNED_TAGS,
258
+ )
259
+
260
+ # ------------------------------------------------------------------
261
+ # Executive/Assistant Pairs (SQL query)
262
+ # ------------------------------------------------------------------
263
+
264
+ def _extract_executive_assistant_pairs(
265
+ self, result: ExtractionResult,
266
+ ) -> list[dict[str, Any]]:
267
+ """Extract executive/assistant pairings via SQL query."""
268
+ try:
269
+ rows = self.conn.execute_sql(
270
+ "SELECT "
271
+ "exec_user.userid AS executive_userid, "
272
+ "asst_user.userid AS assistant_userid, "
273
+ "ea.fkexecutive AS executive_pkid, "
274
+ "ea.fkassistant AS assistant_pkid "
275
+ "FROM executiveassistant ea "
276
+ "JOIN enduser exec_user ON exec_user.pkid = ea.fkexecutive "
277
+ "JOIN enduser asst_user ON asst_user.pkid = ea.fkassistant"
278
+ )
279
+ except Exception as exc:
280
+ msg = f"executiveassistant SQL query failed: {exc}"
281
+ logger.warning("[%s] %s", self.name, msg)
282
+ result.errors.append(msg)
283
+ return []
284
+ result.total += len(rows)
285
+ return rows
286
+
287
+ # ------------------------------------------------------------------
288
+ # Executive/Assistant Settings (SQL query)
289
+ # ------------------------------------------------------------------
290
+
291
+ def _extract_executive_settings(
292
+ self, result: ExtractionResult,
293
+ ) -> list[dict[str, Any]]:
294
+ """Extract executive/assistant service subscriptions via SQL query."""
295
+ try:
296
+ rows = self.conn.execute_sql(
297
+ "SELECT eu.userid, s.name AS service_name, s.servicetype "
298
+ "FROM enduser eu "
299
+ "JOIN endusersubscribedservice euss ON euss.fkenduser = eu.pkid "
300
+ "JOIN subscribedservice s ON s.pkid = euss.fksubscribedservice "
301
+ "WHERE s.name IN ('Executive', 'Executive-Assistant')"
302
+ )
303
+ except Exception as exc:
304
+ msg = f"executive settings SQL query failed: {exc}"
305
+ logger.warning("[%s] %s", self.name, msg)
306
+ result.errors.append(msg)
307
+ return []
308
+ result.total += len(rows)
309
+ return rows
@@ -0,0 +1,73 @@
1
+ """Zeep dict navigation helpers for AXL response parsing.
2
+
3
+ AXL reference fields consistently use the ``{'_value_1': value, 'uuid': uuid}``
4
+ pattern. List fields may be a bare dict (single item) or a list.
5
+
6
+ Sources:
7
+ - 02b-cucm-extraction.md §3 (zeep dict navigation helpers)
8
+ - 02-normalization-architecture.md pass 1 example (reference field pattern)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+
16
+ def ref_value(field: dict[str, Any] | str | None) -> str | None:
17
+ """Extract the display value from a zeep reference field.
18
+
19
+ Zeep reference pattern: ``{'_value_1': 'DP-HQ', 'uuid': '{...}'}``
20
+ Returns ``'DP-HQ'``. Also handles plain strings and None.
21
+ (from 02b §3)
22
+ """
23
+ if field is None:
24
+ return None
25
+ if isinstance(field, str):
26
+ return field
27
+ if isinstance(field, dict):
28
+ return field.get("_value_1")
29
+ return None
30
+
31
+
32
+ def ref_uuid(field: dict[str, Any] | None) -> str | None:
33
+ """Extract the UUID from a zeep reference field.
34
+
35
+ (from 02b §3)
36
+ """
37
+ if isinstance(field, dict):
38
+ return field.get("uuid")
39
+ return None
40
+
41
+
42
+ def to_list(field: dict[str, Any] | list[Any] | None, key: str) -> list[Any]:
43
+ """Normalize a zeep list field to a Python list.
44
+
45
+ Zeep list pattern:
46
+ ``{'line': [item1, item2, ...]}`` — multiple items
47
+ ``{'line': item1}`` — single item (not wrapped in list)
48
+ ``None`` — empty
49
+
50
+ (from 02b §3)
51
+ """
52
+ if field is None:
53
+ return []
54
+ if isinstance(field, dict):
55
+ inner = field.get(key)
56
+ if inner is None:
57
+ return []
58
+ if isinstance(inner, list):
59
+ return inner
60
+ return [inner]
61
+ if isinstance(field, list):
62
+ return field
63
+ return []
64
+
65
+
66
+ def str_to_bool(value: str | None) -> bool | None:
67
+ """Convert AXL string boolean to Python bool.
68
+
69
+ (from 02b §3)
70
+ """
71
+ if value is None:
72
+ return None
73
+ return str(value).lower() == "true"
@@ -0,0 +1,295 @@
1
+ """Tier 3: Informational extraction — CUCM objects with no Webex equivalent.
2
+
3
+ Extracted for assessment report only. No canonical types, no mappers.
4
+ Objects stored as MigrationObject with object_type="info_{type_name}".
5
+
6
+ 20 types across 4 categories:
7
+ - cloud_managed (7): Regions, SRST, MRG, MRL, AAR, Device Mobility, Conference Bridges
8
+ - not_migratable (3): Softkey Templates, IP Phone Services, Intercom DNs
9
+ - different_arch (6): Common Phone Config, Phone Button Templates, Feature Control,
10
+ Credential Policies, Recording Profiles, LDAP Directories
11
+ - planning (4): Application Users, H.323 Gateways, Enterprise Params, Service Params
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ from typing import Any
18
+
19
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # (suffix, axl_list_method, search_criteria, returned_tags, category)
24
+ # These 16 types use standard paginated_list().
25
+ INFORMATIONAL_TYPES: list[tuple[str, str, dict, dict, str]] = [
26
+ # Category 1: Cloud-managed
27
+ ("region", "listRegion", {"name": "%"}, {"name": "", "defaultCodec": ""}, "cloud_managed"),
28
+ ("srst", "listSrst", {"name": "%"}, {"name": "", "ipAddress": "", "port": ""}, "cloud_managed"),
29
+ ("media_resource_group", "listMediaResourceGroup", {"name": "%"}, {"name": "", "description": ""}, "cloud_managed"),
30
+ ("media_resource_list", "listMediaResourceList", {"name": "%"}, {"name": "", "description": ""}, "cloud_managed"),
31
+ ("aar_group", "listAarGroup", {"name": "%"}, {"name": "", "description": ""}, "cloud_managed"),
32
+ ("device_mobility_group", "listDeviceMobilityGroup", {"name": "%"}, {"name": "", "description": ""}, "cloud_managed"),
33
+ ("conference_bridge", "listConferenceBridge", {"name": "%"}, {"name": "", "description": "", "product": ""}, "cloud_managed"),
34
+ # Category 2: Not migratable
35
+ ("ip_phone_service", "listIpPhoneService", {"name": "%"}, {"name": "", "url": "", "serviceType": ""}, "not_migratable"),
36
+ # Category 3: Different architecture
37
+ ("common_phone_config", "listCommonPhoneConfig", {"name": "%"}, {"name": "", "description": ""}, "different_arch"),
38
+ ("phone_button_template", "listPhoneButtonTemplate", {"name": "%"}, {"name": ""}, "different_arch"),
39
+ ("feature_control_policy", "listFeatureControlPolicy", {"name": "%"}, {"name": "", "description": ""}, "different_arch"),
40
+ ("credential_policy", "listCredentialPolicy", {"name": "%"}, {"name": "", "description": ""}, "different_arch"),
41
+ ("recording_profile", "listRecordingProfile", {"name": "%"}, {"name": "", "recorderDestination": ""}, "different_arch"),
42
+ ("ldap_directory", "listLdapDirectory", {"name": "%"}, {"name": "", "ldapDn": ""}, "different_arch"),
43
+ # Category 4: Planning input
44
+ ("app_user", "listAppUser", {"userid": "%"}, {"userid": "", "description": "", "associatedDevices": ""}, "planning"),
45
+ ("h323_gateway", "listH323Gateway", {"name": "%"}, {"name": "", "description": "", "product": ""}, "planning"),
46
+ ]
47
+
48
+ # Category metadata for report rendering
49
+ CATEGORY_METADATA: dict[str, dict[str, str]] = {
50
+ "cloud_managed": {
51
+ "label": "Cloud-Managed Resources",
52
+ "message": "No migration action needed — Webex manages this automatically.",
53
+ },
54
+ "not_migratable": {
55
+ "label": "Feature Gaps",
56
+ "message": "This CUCM feature has no Webex equivalent — functionality will be lost.",
57
+ },
58
+ "different_arch": {
59
+ "label": "Manual Reconfiguration Required",
60
+ "message": "Must be reconfigured manually in Webex Control Hub.",
61
+ },
62
+ "planning": {
63
+ "label": "Migration Planning Inputs",
64
+ "message": "Data informing migration planning decisions.",
65
+ },
66
+ }
67
+
68
+ # Friendly display names for each info type
69
+ DISPLAY_NAMES: dict[str, str] = {
70
+ "region": "Regions",
71
+ "srst": "SRST References",
72
+ "media_resource_group": "Media Resource Groups",
73
+ "media_resource_list": "Media Resource Lists",
74
+ "aar_group": "AAR Groups",
75
+ "device_mobility_group": "Device Mobility Groups",
76
+ "conference_bridge": "Conference Bridges",
77
+ "softkey_template": "Softkey Templates",
78
+ "ip_phone_service": "IP Phone Services",
79
+ "intercom": "Intercom DNs",
80
+ "common_phone_config": "Common Phone Profiles",
81
+ "phone_button_template": "Phone Button Templates",
82
+ "feature_control_policy": "Feature Control Policies",
83
+ "credential_policy": "Credential Policies",
84
+ "recording_profile": "Recording Profiles",
85
+ "ldap_directory": "LDAP Directories",
86
+ "app_user": "Application Users",
87
+ "h323_gateway": "H.323 Gateways",
88
+ "enterprise_params": "Enterprise Parameters",
89
+ "service_params": "Service Parameters",
90
+ }
91
+
92
+ # Webex equivalents for "different_arch" types
93
+ WEBEX_EQUIVALENTS: dict[str, str] = {
94
+ "common_phone_config": "Device configuration templates",
95
+ "phone_button_template": "Line key templates",
96
+ "feature_control_policy": "Calling policies (org/location level)",
97
+ "credential_policy": "SSO / password policies",
98
+ "recording_profile": "Call recording settings",
99
+ "ldap_directory": "Directory sync (Azure AD / Okta / SCIM)",
100
+ }
101
+
102
+ # Workarounds for "not_migratable" types
103
+ NOT_MIGRATABLE_WORKAROUNDS: dict[str, str] = {
104
+ "softkey_template": "9800-series supports PSK; classic MPP uses Webex defaults",
105
+ "ip_phone_service": "Webex app replaces most XML service use cases",
106
+ "intercom": "Speed dial + auto-answer as workaround",
107
+ }
108
+
109
+
110
+ class InformationalExtractor(BaseExtractor):
111
+ """Extract 20 CUCM object types for assessment report only.
112
+
113
+ No canonical types, no mappers, no execution handlers.
114
+ Data flows: extractor -> store -> report appendix.
115
+ """
116
+
117
+ name = "informational"
118
+
119
+ def __init__(self, connection) -> None:
120
+ super().__init__(connection)
121
+ self.results: dict[str, list[dict[str, Any]]] = {}
122
+
123
+ def extract(self) -> ExtractionResult:
124
+ """Run extraction for all 20 informational types."""
125
+ result = ExtractionResult(extractor=self.name)
126
+
127
+ # 1. Standard AXL list types (16 types)
128
+ for suffix, method, criteria, tags, category in INFORMATIONAL_TYPES:
129
+ items = self._extract_list_type(suffix, method, criteria, tags, category, result)
130
+ self.results[suffix] = items
131
+
132
+ # Enrich common_phone_config with vendorConfig detail
133
+ cpcs = self.results.get("common_phone_config", [])
134
+ if cpcs:
135
+ self._fetch_common_phone_config_details(cpcs, result)
136
+
137
+ # 2. Softkey templates via SQL (AXL list doesn't exist)
138
+ self.results["softkey_template"] = self._extract_softkey_templates(result)
139
+
140
+ # 3. Intercom DNs via SQL
141
+ self.results["intercom"] = self._extract_intercom_dns(result)
142
+
143
+ # 4. Enterprise parameters (single getEnterprise call)
144
+ self.results["enterprise_params"] = self._extract_enterprise_params(result)
145
+
146
+ # 5. Service parameters (filtered listProcessConfig)
147
+ self.results["service_params"] = self._extract_service_params(result)
148
+
149
+ logger.info(
150
+ "[%s] Extracted %d informational objects across %d types (%d failed)",
151
+ self.name,
152
+ result.total,
153
+ sum(1 for v in self.results.values() if v),
154
+ result.failed,
155
+ )
156
+ return result
157
+
158
+ def _fetch_common_phone_config_details(
159
+ self, items: list[dict[str, Any]], result: ExtractionResult,
160
+ ) -> None:
161
+ """Enrich common_phone_config items with vendorConfig from getCommonPhoneConfig."""
162
+ for item in items:
163
+ name = item.get("name")
164
+ if not name:
165
+ continue
166
+ try:
167
+ resp = self.conn.service.getCommonPhoneConfig(name=name)
168
+ detail = resp
169
+ if isinstance(resp, dict):
170
+ detail = resp.get("return", resp)
171
+ if isinstance(detail, dict):
172
+ detail = detail.get("commonPhoneConfig", detail)
173
+ if isinstance(detail, dict):
174
+ vendor_config = detail.get("vendorConfig")
175
+ if vendor_config is not None:
176
+ item["vendorConfig"] = (
177
+ str(vendor_config) if not isinstance(vendor_config, str)
178
+ else vendor_config
179
+ )
180
+ for field in ("dndOption", "dndAlertingType"):
181
+ val = detail.get(field)
182
+ if val is not None:
183
+ item[field] = val
184
+ except Exception as exc:
185
+ logger.debug(
186
+ "[%s] getCommonPhoneConfig(%s) failed: %s", self.name, name, exc
187
+ )
188
+
189
+ def _extract_list_type(
190
+ self,
191
+ suffix: str,
192
+ method: str,
193
+ criteria: dict,
194
+ tags: dict,
195
+ category: str,
196
+ result: ExtractionResult,
197
+ ) -> list[dict[str, Any]]:
198
+ """Extract a single AXL list type."""
199
+ try:
200
+ items = self.paginated_list(method, criteria, tags)
201
+ except Exception as exc:
202
+ logger.warning("[%s] %s failed: %s", self.name, method, exc)
203
+ result.errors.append(f"{method} failed: {exc}")
204
+ result.failed += 1
205
+ return []
206
+
207
+ result.total += len(items)
208
+ for item in items:
209
+ item["_category"] = category
210
+ item["_info_type"] = suffix
211
+ return items
212
+
213
+ def _extract_softkey_templates(self, result: ExtractionResult) -> list[dict[str, Any]]:
214
+ """Extract softkey template names via SQL (AXL list doesn't exist in CUCM 15.0)."""
215
+ try:
216
+ rows = self.conn.execute_sql(
217
+ "SELECT name, description FROM softkeytemplate ORDER BY name"
218
+ )
219
+ except Exception as exc:
220
+ logger.warning("[%s] Softkey template SQL failed: %s", self.name, exc)
221
+ result.errors.append(f"Softkey template SQL failed: {exc}")
222
+ return []
223
+
224
+ result.total += len(rows)
225
+ for row in rows:
226
+ row["_category"] = "not_migratable"
227
+ row["_info_type"] = "softkey_template"
228
+ return rows
229
+
230
+ def _extract_intercom_dns(self, result: ExtractionResult) -> list[dict[str, Any]]:
231
+ """Extract intercom DNs via SQL query."""
232
+ try:
233
+ rows = self.conn.execute_sql(
234
+ "SELECT dnorpattern, description, fkroutepartition "
235
+ "FROM numplan WHERE tkpatternusage = 9 ORDER BY dnorpattern"
236
+ )
237
+ except Exception as exc:
238
+ logger.warning("[%s] Intercom DN SQL failed: %s", self.name, exc)
239
+ result.errors.append(f"Intercom DN SQL failed: {exc}")
240
+ return []
241
+
242
+ result.total += len(rows)
243
+ for row in rows:
244
+ row["_category"] = "not_migratable"
245
+ row["_info_type"] = "intercom"
246
+ return rows
247
+
248
+ def _extract_enterprise_params(self, result: ExtractionResult) -> list[dict[str, Any]]:
249
+ """Extract enterprise parameters via single getEnterprise call."""
250
+ try:
251
+ raw = self.conn.service.getEnterprise()
252
+ if raw is None:
253
+ return []
254
+ from zeep.helpers import serialize_object
255
+ params = serialize_object(raw, dict)
256
+ if not isinstance(params, dict):
257
+ params = {"raw": str(params)}
258
+ except Exception as exc:
259
+ logger.warning("[%s] getEnterprise failed: %s", self.name, exc)
260
+ result.errors.append(f"getEnterprise failed: {exc}")
261
+ return []
262
+
263
+ result.total += 1
264
+ params["_category"] = "planning"
265
+ params["_info_type"] = "enterprise_params"
266
+ params["name"] = "Enterprise Parameters"
267
+ return [params]
268
+
269
+ def _extract_service_params(self, result: ExtractionResult) -> list[dict[str, Any]]:
270
+ """Extract telephony-related service parameters via listProcessConfig."""
271
+ try:
272
+ items = self.paginated_list(
273
+ "listProcessConfig",
274
+ {"name": "%"},
275
+ {"name": "", "service": "", "value": ""},
276
+ )
277
+ except Exception as exc:
278
+ logger.warning("[%s] listProcessConfig failed: %s", self.name, exc)
279
+ result.errors.append(f"listProcessConfig failed: {exc}")
280
+ return []
281
+
282
+ telephony_keywords = (
283
+ "callmanager", "telephony", "tftp", "certificate",
284
+ "cisco call", "cisco ip voice",
285
+ )
286
+ filtered = []
287
+ for item in items:
288
+ service = (item.get("service") or "").lower()
289
+ if any(kw in service for kw in telephony_keywords):
290
+ item["_category"] = "planning"
291
+ item["_info_type"] = "service_params"
292
+ filtered.append(item)
293
+
294
+ result.total += len(filtered)
295
+ return filtered