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,229 @@
1
+ """Template extractor — phone button templates and softkey templates.
2
+
3
+ Phone button templates use two-step AXL extraction (list → get-detail).
4
+ Softkey templates use SQL queries (AXL list/get operations don't exist in v15.0).
5
+
6
+ Templates are shared objects — typically 5-20 button templates and
7
+ 3-10 softkey templates per cluster.
8
+
9
+ AXL gotchas (verified 2026-03-25 against CUCM 15.0):
10
+ - listPhoneButtonTemplate returnedTags only supports {name, isUserModifiable},
11
+ NOT basePhoneTemplateName.
12
+ - getPhoneButtonTemplate returns buttons with <buttonNumber> (not <index>),
13
+ and "Speed Dial BLF" as the feature name (not "Busy Lamp Field" as in add).
14
+ - addSoftkeyTemplate, getSoftkeyTemplate, listSoftkeyTemplate DO NOT EXIST.
15
+ Softkey templates must be queried via executeSQLQuery on the softkeytemplate
16
+ table, with typesoftkey for ID-to-name decoding.
17
+
18
+ Sources:
19
+ - tier2-phase2-phone-config-design.md §1.1, §13.1, §13.2
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ from typing import Any
26
+
27
+ from wxcli.migration.cucm.connection import AXLConnection
28
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
29
+ from wxcli.migration.cucm.extractors.helpers import ref_value
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # listPhoneButtonTemplate only supports these returnedTags (verified 2026-03-25)
34
+ BUTTON_TEMPLATE_LIST_TAGS = {"name": "", "isUserModifiable": ""}
35
+
36
+
37
+ # Softkey ID → name mapping, populated once from typesoftkey SQL table
38
+ _SOFTKEY_ID_CACHE: dict[str, str] = {}
39
+
40
+ # softkeysetclause state positions (verified 2026-03-25, CUCM 15.0)
41
+ # Semicolon-delimited, 12 states in fixed order:
42
+ SOFTKEY_STATE_ORDER = [
43
+ "On Hook",
44
+ "Connected",
45
+ "On Hold",
46
+ "Ring In",
47
+ "Off Hook",
48
+ "Connected Transfer",
49
+ "Transition",
50
+ "Connected Conference",
51
+ "Park",
52
+ "Alerting",
53
+ "Shared Active",
54
+ "Shared Held",
55
+ ]
56
+
57
+
58
+ class TemplateExtractor(BaseExtractor):
59
+ """Extract phone button templates and softkey templates from CUCM."""
60
+
61
+ name = "templates"
62
+
63
+ def __init__(self, connection: AXLConnection) -> None:
64
+ super().__init__(connection)
65
+ self.results: dict[str, list[dict[str, Any]]] = {}
66
+
67
+ def extract(self) -> ExtractionResult:
68
+ """Run extraction for button templates (AXL) and softkey templates (SQL)."""
69
+ result = ExtractionResult(extractor=self.name)
70
+
71
+ button_templates = self._extract_button_templates(result)
72
+ softkey_templates = self._extract_softkey_templates(result)
73
+
74
+ self.results["button_templates"] = button_templates
75
+ self.results["softkey_templates"] = softkey_templates
76
+
77
+ logger.info(
78
+ "[%s] Extracted %d button templates, %d softkey templates (%d failed)",
79
+ self.name, len(button_templates), len(softkey_templates), result.failed,
80
+ )
81
+ return result
82
+
83
+ # ------------------------------------------------------------------
84
+ # Phone button templates — AXL list + get-detail
85
+ # ------------------------------------------------------------------
86
+
87
+ def _extract_button_templates(self, result: ExtractionResult) -> list[dict[str, Any]]:
88
+ """List and get-detail all phone button templates."""
89
+ logger.info("[%s] Listing phone button templates...", self.name)
90
+ try:
91
+ summaries = self.paginated_list(
92
+ method_name="listPhoneButtonTemplate",
93
+ search_criteria={"name": "%"},
94
+ returned_tags=BUTTON_TEMPLATE_LIST_TAGS,
95
+ )
96
+ except Exception as exc:
97
+ logger.warning("[%s] listPhoneButtonTemplate failed: %s", self.name, exc)
98
+ result.errors.append(f"listPhoneButtonTemplate failed: {exc}")
99
+ return []
100
+
101
+ result.total += len(summaries)
102
+
103
+ templates: list[dict[str, Any]] = []
104
+ for summary in summaries:
105
+ name = ref_value(summary.get("name")) or summary.get("name")
106
+ if not name:
107
+ result.failed += 1
108
+ result.errors.append("Button template with no name")
109
+ continue
110
+
111
+ try:
112
+ detail = self.get_detail("getPhoneButtonTemplate", name=name)
113
+ except Exception as exc:
114
+ logger.warning("[%s] getPhoneButtonTemplate(%s) failed: %s", self.name, name, exc)
115
+ result.failed += 1
116
+ result.errors.append(f"getPhoneButtonTemplate failed for {name}")
117
+ continue
118
+
119
+ if detail is None:
120
+ result.failed += 1
121
+ result.errors.append(f"getPhoneButtonTemplate returned None for {name}")
122
+ continue
123
+
124
+ templates.append(detail)
125
+
126
+ return templates
127
+
128
+ # ------------------------------------------------------------------
129
+ # Softkey templates — SQL queries (AXL operations don't exist)
130
+ # ------------------------------------------------------------------
131
+
132
+ def _extract_softkey_templates(self, result: ExtractionResult) -> list[dict[str, Any]]:
133
+ """Extract softkey templates via SQL (AXL list/get don't exist in v15.0)."""
134
+ logger.info("[%s] Querying softkey templates via SQL...", self.name)
135
+
136
+ # Load softkey ID → name mapping
137
+ softkey_names = self._load_softkey_type_names()
138
+
139
+ try:
140
+ rows = self.conn.execute_sql(
141
+ "SELECT name, description, softkeyclause, softkeysetclause, "
142
+ "iksoftkeytemplate_base FROM softkeytemplate ORDER BY name"
143
+ )
144
+ except Exception as exc:
145
+ logger.warning("[%s] Softkey template SQL query failed: %s", self.name, exc)
146
+ result.errors.append(f"Softkey template SQL failed: {exc}")
147
+ return []
148
+
149
+ result.total += len(rows)
150
+ templates: list[dict[str, Any]] = []
151
+
152
+ for row in rows:
153
+ name = row.get("name")
154
+ if not name:
155
+ result.failed += 1
156
+ continue
157
+
158
+ try:
159
+ parsed = self._parse_softkey_template(row, softkey_names)
160
+ templates.append(parsed)
161
+ except Exception as exc:
162
+ logger.warning("[%s] Failed to parse softkey template %s: %s", self.name, name, exc)
163
+ result.failed += 1
164
+ result.errors.append(f"Parse failed for softkey template {name}: {exc}")
165
+
166
+ return templates
167
+
168
+ def _load_softkey_type_names(self) -> dict[str, str]:
169
+ """Load typesoftkey enum → name mapping from CUCM DB."""
170
+ if _SOFTKEY_ID_CACHE:
171
+ return _SOFTKEY_ID_CACHE
172
+
173
+ try:
174
+ rows = self.conn.execute_sql(
175
+ "SELECT enum, name FROM typesoftkey ORDER BY enum"
176
+ )
177
+ for row in rows:
178
+ enum_val = str(row.get("enum", ""))
179
+ sk_name = row.get("name", "")
180
+ if enum_val and sk_name:
181
+ _SOFTKEY_ID_CACHE[enum_val] = sk_name
182
+ except Exception as exc:
183
+ logger.warning("[%s] typesoftkey query failed: %s", self.name, exc)
184
+
185
+ return _SOFTKEY_ID_CACHE
186
+
187
+ def _parse_softkey_template(
188
+ self, row: dict[str, Any], softkey_names: dict[str, str]
189
+ ) -> dict[str, Any]:
190
+ """Parse a softkeytemplate SQL row into a structured dict.
191
+
192
+ Decodes softkeyclause and softkeysetclause into human-readable form.
193
+ """
194
+ name = row.get("name", "")
195
+ description = row.get("description", "")
196
+ base_pkid = row.get("iksoftkeytemplate_base", "")
197
+
198
+ # Decode softkeyclause: "0:1:2:3:4" → ["Undefined", "Redial", "NewCall", ...]
199
+ softkey_clause = row.get("softkeyclause", "")
200
+ available_softkeys: list[str] = []
201
+ if softkey_clause:
202
+ for sk_id in softkey_clause.split(":"):
203
+ sk_name = softkey_names.get(sk_id, f"unknown_{sk_id}")
204
+ available_softkeys.append(sk_name)
205
+
206
+ # Decode softkeysetclause: semicolon-separated per-state lists
207
+ # Each state: colon-separated softkey IDs
208
+ softkey_set_clause = row.get("softkeysetclause", "")
209
+ call_states: dict[str, list[str]] = {}
210
+ if softkey_set_clause:
211
+ state_parts = softkey_set_clause.split(";")
212
+ for i, state_data in enumerate(state_parts):
213
+ if i >= len(SOFTKEY_STATE_ORDER):
214
+ break
215
+ state_name = SOFTKEY_STATE_ORDER[i]
216
+ keys: list[str] = []
217
+ if state_data:
218
+ for sk_id in state_data.split(":"):
219
+ sk_name = softkey_names.get(sk_id, f"unknown_{sk_id}")
220
+ keys.append(sk_name)
221
+ call_states[state_name] = keys
222
+
223
+ return {
224
+ "name": name,
225
+ "description": description,
226
+ "base_template_pkid": base_pkid,
227
+ "available_softkeys": available_softkeys,
228
+ "call_states": call_states,
229
+ }
@@ -0,0 +1,173 @@
1
+ """Tier 4 feature gap extractor — recording profiles, remote destinations,
2
+ transformation patterns, extension mobility device profiles.
3
+
4
+ Wave 1: extract + flag only. No mappers or execution handlers.
5
+ (from docs/prompts/tier4-feature-gap-extraction.md)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any
12
+
13
+ from wxcli.migration.cucm.connection import AXLConnection
14
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ RECORDING_PROFILE_TAGS = {"name": "", "recordingCssName": "", "recorderDestination": ""}
19
+ REMOTE_DEST_PROFILE_TAGS = {"name": "", "description": "", "userId": ""}
20
+ CALLING_PARTY_XFORM_TAGS = {
21
+ "pattern": "", "description": "", "callingSearchSpaceName": "",
22
+ "routePartitionName": "", "callingPartyTransformationMask": "",
23
+ "callingPartyPrefixDigits": "", "digitDiscardInstructionName": "",
24
+ }
25
+ CALLED_PARTY_XFORM_TAGS = {
26
+ "pattern": "", "description": "", "callingSearchSpaceName": "",
27
+ "routePartitionName": "", "calledPartyTransformationMask": "",
28
+ "calledPartyPrefixDigits": "", "digitDiscardInstructionName": "",
29
+ }
30
+ DEVICE_PROFILE_TAGS = {"name": "", "description": "", "product": "", "protocol": ""}
31
+ _BLOCKED_PARTITION_SQL = """\
32
+ SELECT n.dnorpattern, rp.name as partition_name, eu.userid
33
+ FROM numplan n
34
+ JOIN routepartition rp ON rp.pkid = n.fkroutepartition
35
+ LEFT JOIN devicenumplanmap dnpm ON dnpm.fknumplan = n.pkid
36
+ LEFT JOIN device d ON d.pkid = dnpm.fkdevice
37
+ LEFT JOIN enduser eu ON eu.pkid = d.fkenduser
38
+ WHERE LOWER(rp.name) LIKE '%intercept%'
39
+ OR LOWER(rp.name) LIKE '%block%'
40
+ OR LOWER(rp.name) LIKE '%out_of_service%'
41
+ OR LOWER(rp.name) LIKE '%oos%'
42
+ """
43
+
44
+ _CFA_VOICEMAIL_SQL = """\
45
+ SELECT n.dnorpattern, rp.name as partition_name,
46
+ cfwd.cfadestination, eu.userid
47
+ FROM numplan n
48
+ LEFT JOIN routepartition rp ON rp.pkid = n.fkroutepartition
49
+ JOIN callforwarddynamic cfwd ON cfwd.fknumplan = n.pkid
50
+ LEFT JOIN devicenumplanmap dnpm ON dnpm.fknumplan = n.pkid
51
+ LEFT JOIN device d ON d.pkid = dnpm.fkdevice
52
+ LEFT JOIN enduser eu ON eu.pkid = d.fkenduser
53
+ WHERE cfwd.cfadestination IS NOT NULL
54
+ AND cfwd.cfadestination != ''
55
+ AND cfwd.cfavoicemailenabled = 't'
56
+ AND NOT EXISTS (
57
+ SELECT 1 FROM device d2
58
+ JOIN devicenumplanmap dnpm2 ON dnpm2.fkdevice = d2.pkid
59
+ WHERE dnpm2.fknumplan = n.pkid
60
+ AND d2.tkclass = 1
61
+ AND d2.tkstatus_registrationstate = 2
62
+ )
63
+ """
64
+
65
+
66
+ class Tier4Extractor(BaseExtractor):
67
+ """Extract Tier 4 feature gap data from CUCM.
68
+
69
+ Extracts:
70
+ - Recording profiles (Item 1)
71
+ - Remote destination profiles / SNR (Item 2)
72
+ - Calling/Called party transformation patterns (Item 4)
73
+ - Extension Mobility device profiles (Item 6)
74
+ """
75
+
76
+ name = "tier4"
77
+
78
+ def __init__(self, connection: AXLConnection) -> None:
79
+ super().__init__(connection)
80
+ self.results: dict[str, list[dict[str, Any]]] = {}
81
+
82
+ def extract(self) -> ExtractionResult:
83
+ result = ExtractionResult(extractor=self.name)
84
+ total = 0
85
+ total += self._extract_type(
86
+ result, "recording_profiles",
87
+ "listRecordingProfile", {"name": "%"}, RECORDING_PROFILE_TAGS,
88
+ )
89
+ total += self._extract_type(
90
+ result, "remote_destination_profiles",
91
+ "listRemoteDestinationProfile", {"name": "%"}, REMOTE_DEST_PROFILE_TAGS,
92
+ )
93
+ total += self._extract_type(
94
+ result, "calling_party_transformations",
95
+ "listCallingPartyTransformationPattern", {"pattern": "%"},
96
+ CALLING_PARTY_XFORM_TAGS,
97
+ )
98
+ total += self._extract_type(
99
+ result, "called_party_transformations",
100
+ "listCalledPartyTransformationPattern", {"pattern": "%"},
101
+ CALLED_PARTY_XFORM_TAGS,
102
+ )
103
+ total += self._extract_type(
104
+ result, "device_profiles",
105
+ "listDeviceProfile", {"name": "%"}, DEVICE_PROFILE_TAGS,
106
+ )
107
+ total += self._extract_intercept_candidates(result)
108
+ result.total = total
109
+ return result
110
+
111
+ def _extract_type(
112
+ self,
113
+ result: ExtractionResult,
114
+ key: str,
115
+ method: str,
116
+ search_criteria: dict[str, str],
117
+ returned_tags: dict[str, str],
118
+ ) -> int:
119
+ try:
120
+ items = self.paginated_list(method, search_criteria, returned_tags)
121
+ except Exception as exc:
122
+ msg = f"{method} failed: {exc}"
123
+ logger.warning("[%s] %s", self.name, msg)
124
+ result.errors.append(msg)
125
+ self.results[key] = []
126
+ return 0
127
+ self.results[key] = items
128
+ logger.info("[%s] %s: %d objects", self.name, key, len(items))
129
+ return len(items)
130
+
131
+ def _extract_intercept_candidates(self, result: ExtractionResult) -> int:
132
+ """Detect intercept-like configurations via SQL heuristics."""
133
+ seen: dict[str, dict[str, Any]] = {}
134
+ try:
135
+ rows = self.conn.execute_sql(_BLOCKED_PARTITION_SQL)
136
+ for row in rows:
137
+ dn = row.get("dnorpattern", "")
138
+ if not dn:
139
+ continue
140
+ seen[dn] = {
141
+ "userid": row.get("userid") or "",
142
+ "dn": dn,
143
+ "partition": row.get("partition_name") or "",
144
+ "signal_type": "blocked_partition",
145
+ "forward_destination": "",
146
+ "voicemail_enabled": False,
147
+ }
148
+ except Exception as exc:
149
+ msg = f"Intercept blocked-partition SQL failed: {exc}"
150
+ logger.warning("[%s] %s", self.name, msg)
151
+ result.errors.append(msg)
152
+ try:
153
+ rows = self.conn.execute_sql(_CFA_VOICEMAIL_SQL)
154
+ for row in rows:
155
+ dn = row.get("dnorpattern", "")
156
+ if not dn or dn in seen:
157
+ continue
158
+ seen[dn] = {
159
+ "userid": row.get("userid") or "",
160
+ "dn": dn,
161
+ "partition": row.get("partition_name") or "",
162
+ "signal_type": "cfa_voicemail",
163
+ "forward_destination": row.get("cfadestination") or "",
164
+ "voicemail_enabled": True,
165
+ }
166
+ except Exception as exc:
167
+ msg = f"Intercept CFA-voicemail SQL failed: {exc}"
168
+ logger.warning("[%s] %s", self.name, msg)
169
+ result.errors.append(msg)
170
+ candidates = list(seen.values())
171
+ self.results["intercept_candidates"] = candidates
172
+ logger.info("[%s] intercept_candidates: %d objects", self.name, len(candidates))
173
+ return len(candidates)
@@ -0,0 +1,240 @@
1
+ """Users extractor — End Users.
2
+
3
+ Extracts CUCM End Users with associated device references, directory info,
4
+ and voicemail associations. Feeds user_mapper (03b §2), css_mapper
5
+ (cross-ref #18), and voicemail_mapper (cross-ref #26).
6
+
7
+ Sources:
8
+ - 02b-cucm-extraction.md §2.2 (users extractor spec)
9
+ - 03b-transform-mappers.md §2 (user_mapper field mapping)
10
+ - 02-normalization-architecture.md cross-ref #3, #4, #18, #26
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any
17
+
18
+ from rich.console import Console
19
+
20
+ from wxcli.migration.cucm.connection import AXLConnection
21
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
22
+ from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
23
+
24
+ logger = logging.getLogger(__name__)
25
+ console = Console()
26
+
27
+ # --- returnedTags constants (from 02b §2.2) ---
28
+
29
+ USER_LIST_RETURNED_TAGS = {
30
+ 'firstName': '',
31
+ 'lastName': '',
32
+ 'userid': '',
33
+ 'mailid': '',
34
+ }
35
+
36
+ USER_GET_RETURNED_TAGS = {
37
+ 'firstName': '', # Maps to Webex firstName
38
+ 'lastName': '', # Maps to Webex lastName
39
+ 'mailid': '', # Maps to Webex emails[] primary
40
+ 'userid': '', # Fallback for emails[] if mailid empty
41
+ 'telephoneNumber': '', # User's primary phone number
42
+ 'department': '', # Maps to Webex department
43
+ 'title': '', # Maps to Webex title
44
+ 'manager': '', # Maps to Webex manager/managerId
45
+ 'directoryUri': '', # Informational — "Webex auto-assigns SIP address"
46
+ 'userLocale': '', # Informational — "Not a People API field"
47
+ 'selfService': '', # Voicemail pilot — "Mapped by voicemail_mapper"
48
+ 'associatedDevices': '', # Cross-ref #3: user_has_device
49
+ 'enableCti': '', # CTI-enabled flag
50
+ 'primaryExtension': '', # Cross-ref #4: user_has_primary_dn
51
+ 'callingSearchSpaceName': '', # Cross-ref #18: user_has_css
52
+ 'voiceMailProfile': '', # Cross-ref #26: user_has_voicemail_profile
53
+ }
54
+
55
+
56
+ class UserExtractor(BaseExtractor):
57
+ """Extract CUCM End Users.
58
+
59
+ Two-step extraction:
60
+ 1. listEndUser discovers all user PKIDs (summary fields only)
61
+ 2. getEndUser per user retrieves the full field set including
62
+ associatedDevices, primaryExtension, CSS, and voicemail profile
63
+
64
+ Results stored in self.results['users'].
65
+
66
+ (from 02b §2.2)
67
+ """
68
+
69
+ name = "users"
70
+
71
+ def __init__(self, connection: AXLConnection) -> None:
72
+ super().__init__(connection)
73
+ self.results: dict[str, list[dict[str, Any]]] = {
74
+ 'users': [],
75
+ }
76
+
77
+ def extract(self) -> ExtractionResult:
78
+ """Discover users via AXL listEndUser, then get full detail.
79
+
80
+ Falls back to executeSQLQuery if listEndUser is blocked (EPR error).
81
+ This happens on cloud-connected CUCMs and some LDAP-synced deployments.
82
+ """
83
+ try:
84
+ return self._extract_via_axl()
85
+ except Exception as exc:
86
+ exc_str = str(exc)
87
+ # EPR errors: cloud-connected CUCM or service not fully activated
88
+ # AttributeError: WSDL doesn't define EndUser operations (thin AXL)
89
+ # Fault: AXL service rejects the method
90
+ if any(s in exc_str for s in ("EPR", "Operation not found", "has no operation")):
91
+ logger.warning(
92
+ "listEndUser AXL unavailable (%s) — falling back to SQL",
93
+ exc,
94
+ )
95
+ return self._extract_via_sql()
96
+ raise
97
+
98
+ def _extract_via_axl(self) -> ExtractionResult:
99
+ """Standard AXL extraction: listEndUser → getEndUser."""
100
+ errors: list[str] = []
101
+ failed = 0
102
+
103
+ summary_list = self.paginated_list(
104
+ "listEndUser",
105
+ search_criteria={"firstName": "%"},
106
+ returned_tags=USER_LIST_RETURNED_TAGS,
107
+ )
108
+ total = len(summary_list)
109
+ logger.info("Found %d end users via listEndUser", total)
110
+
111
+ for i, user_summary in enumerate(summary_list, 1):
112
+ if i % 200 == 0:
113
+ console.print(f" users: {i}/{total}...")
114
+ userid = user_summary.get("userid", "<unknown>")
115
+ try:
116
+ raw = self.get_detail("getEndUser", userid=userid)
117
+ if raw is None:
118
+ failed += 1
119
+ errors.append(f"getEndUser returned None for '{userid}'")
120
+ continue
121
+ raw["associatedDevices"] = to_list(
122
+ raw.get("associatedDevices"), "device"
123
+ )
124
+ self.results["users"].append(raw)
125
+ except Exception as exc:
126
+ failed += 1
127
+ errors.append(f"getEndUser failed for '{userid}': {exc}")
128
+
129
+ return ExtractionResult(
130
+ extractor=self.name, total=total, failed=failed, errors=errors,
131
+ )
132
+
133
+ def _extract_via_sql(self) -> ExtractionResult:
134
+ """SQL fallback when AXL EndUser operations are blocked.
135
+
136
+ Uses executeSQLQuery to read the enduser table directly. Joins to
137
+ enduserdevicemap + device for associatedDevices, and numplan for
138
+ primaryExtension. Produces dicts matching the AXL getEndUser shape
139
+ so downstream normalizers work identically.
140
+
141
+ Verified against live CUCM 15.0 (2026-03-23): executeSQLQuery works
142
+ even when listEndUser/getEndUser return EPR errors.
143
+ """
144
+ errors: list[str] = []
145
+
146
+ # Step 1: Get all active end users (exclude system/token users)
147
+ user_rows = self.conn.execute_sql(
148
+ "SELECT eu.pkid, eu.userid, eu.firstname, eu.lastname, "
149
+ "eu.mailid, eu.telephonenumber, eu.department, eu.title, "
150
+ "eu.manager, eu.directoryuri, eu.allowcticontrolflag "
151
+ "FROM enduser eu "
152
+ "WHERE eu.status = 1 AND eu.userid NOT LIKE 'Token_User_%'"
153
+ )
154
+ logger.info("Found %d end users via SQL", len(user_rows))
155
+
156
+ # Step 2: Get device associations (user → phone name)
157
+ # Try enduserdevicemap first (explicit associations), then fall back
158
+ # to device.fkenduser (ownerUserName). Both represent user→device links
159
+ # but enduserdevicemap is only populated when devices are added to a
160
+ # user's "Controlled Devices" list via GUI/updateEndUser.
161
+ device_rows = self.conn.execute_sql(
162
+ "SELECT eu.userid, d.name AS device_name "
163
+ "FROM enduser eu "
164
+ "JOIN device d ON d.fkenduser = eu.pkid "
165
+ "WHERE eu.status = 1 "
166
+ "UNION "
167
+ "SELECT eu.userid, d.name AS device_name "
168
+ "FROM enduser eu "
169
+ "JOIN enduserdevicemap edm ON eu.pkid = edm.fkenduser "
170
+ "JOIN device d ON edm.fkdevice = d.pkid "
171
+ "WHERE eu.status = 1"
172
+ )
173
+ user_devices: dict[str, list[str]] = {}
174
+ for row in device_rows:
175
+ uid = row.get("userid", "")
176
+ dname = row.get("device_name", "")
177
+ if uid and dname:
178
+ user_devices.setdefault(uid, []).append(dname)
179
+
180
+ # Step 3: Get primary extensions (line 1 of first physical phone)
181
+ # Uses device.fkenduser (ownerUserName) — same as step 2.
182
+ # Prefers physical phones (SEP*) over softphones (CSF*) for primary DN.
183
+ primary_dn_rows = self.conn.execute_sql(
184
+ "SELECT eu.userid, np.dnorpattern AS pattern, rp.name AS partition "
185
+ "FROM enduser eu "
186
+ "JOIN device d ON d.fkenduser = eu.pkid "
187
+ "JOIN devicenumplanmap dnmap ON d.pkid = dnmap.fkdevice "
188
+ "JOIN numplan np ON dnmap.fknumplan = np.pkid "
189
+ "LEFT JOIN routepartition rp ON np.fkroutepartition = rp.pkid "
190
+ "WHERE eu.status = 1 AND dnmap.numplanindex = 1 "
191
+ "AND d.name NOT LIKE 'CSF%'"
192
+ )
193
+ user_primary_dn: dict[str, dict[str, str | None]] = {}
194
+ for row in primary_dn_rows:
195
+ uid = row.get("userid", "")
196
+ if uid and uid not in user_primary_dn:
197
+ user_primary_dn[uid] = {
198
+ "pattern": row.get("pattern"),
199
+ "routePartitionName": row.get("partition"),
200
+ }
201
+
202
+ # Step 4: Build user dicts matching AXL getEndUser shape
203
+ for row in user_rows:
204
+ userid = row.get("userid", "")
205
+ user_dict: dict[str, Any] = {
206
+ "pkid": row.get("pkid"),
207
+ "firstName": row.get("firstname"),
208
+ "lastName": row.get("lastname"),
209
+ "mailid": row.get("mailid"),
210
+ "userid": userid,
211
+ "telephoneNumber": row.get("telephonenumber"),
212
+ "department": row.get("department"),
213
+ "title": row.get("title"),
214
+ "manager": row.get("manager"),
215
+ "directoryUri": row.get("directoryuri"),
216
+ "enableCti": row.get("allowcticontrolflag"),
217
+ "associatedDevices": user_devices.get(userid, []),
218
+ "primaryExtension": user_primary_dn.get(userid),
219
+ # These require AXL EndUser object — not available via SQL
220
+ "callingSearchSpaceName": None,
221
+ "voiceMailProfile": None,
222
+ "selfService": None,
223
+ "userLocale": None,
224
+ "_extracted_via": "sql",
225
+ }
226
+ self.results["users"].append(user_dict)
227
+
228
+ logger.info(
229
+ "End users via SQL: %d extracted, %d with devices, %d with primary DN",
230
+ len(user_rows),
231
+ sum(1 for u in self.results["users"] if u["associatedDevices"]),
232
+ sum(1 for u in self.results["users"] if u["primaryExtension"]),
233
+ )
234
+
235
+ return ExtractionResult(
236
+ extractor=self.name,
237
+ total=len(user_rows),
238
+ failed=0,
239
+ errors=errors,
240
+ )