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,243 @@
1
+ """Voicemail extractor for CUCM AXL.
2
+
3
+ Pulls Voicemail Profiles and Voicemail Pilots from CUCM AXL.
4
+ Optionally extracts per-user Unity Connection settings via CUPI.
5
+
6
+ Sources:
7
+ - 02b-cucm-extraction.md §2.6 (voicemail 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
+ from wxcli.migration.cucm.unity_connection import UnityConnectionClient
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # ------------------------------------------------------------------
23
+ # ReturnedTags constants
24
+ # ------------------------------------------------------------------
25
+
26
+ # Verified: listVoiceMailProfile does NOT accept voiceMailPilot.
27
+ # Use getVoiceMailProfile for pilot reference.
28
+ VOICEMAIL_PROFILE_LIST_RETURNED_TAGS = {
29
+ "name": "", "description": "", "voiceMailboxMask": "",
30
+ }
31
+
32
+ # Verified: listVoiceMailPilot CSS field is 'cssName', NOT 'callingSearchSpaceName'.
33
+ VOICEMAIL_PILOT_LIST_RETURNED_TAGS = {
34
+ "dirn": "", "description": "",
35
+ }
36
+
37
+
38
+ class VoicemailExtractor(BaseExtractor):
39
+ """Extract voicemail configuration from CUCM AXL.
40
+
41
+ Covers Voicemail Profiles and Voicemail Pilots.
42
+ Optionally fetches per-user voicemail settings from Unity Connection
43
+ via the CUPI REST API when a ``unity_client`` is provided.
44
+
45
+ (from 02b §2.6)
46
+ """
47
+
48
+ name = "voicemail"
49
+
50
+ def __init__(
51
+ self,
52
+ connection: AXLConnection,
53
+ unity_client: UnityConnectionClient | None = None,
54
+ ) -> None:
55
+ super().__init__(connection)
56
+ self.unity_client = unity_client
57
+ self.results: dict[str, list[dict[str, Any]]] = {}
58
+
59
+ def extract(self) -> ExtractionResult:
60
+ """Run voicemail extraction (AXL profiles and pilots).
61
+
62
+ Unity Connection per-user settings are extracted separately via
63
+ ``extract_unity_settings()`` because the user list comes from
64
+ the discovery extractor.
65
+
66
+ Returns an ExtractionResult summarizing total objects and errors.
67
+ """
68
+ result = ExtractionResult(extractor=self.name)
69
+
70
+ self.results["voicemail_profiles"] = self._extract_voicemail_profiles(
71
+ result
72
+ )
73
+ self.results["voicemail_pilots"] = self._extract_voicemail_pilots(
74
+ result
75
+ )
76
+
77
+ # Shared/group mailboxes (Unity Connection only — skipped without client)
78
+ self.results["shared_mailboxes"] = self._extract_shared_mailboxes(result)
79
+
80
+ return result
81
+
82
+ # ------------------------------------------------------------------
83
+ # Voicemail Profiles
84
+ # ------------------------------------------------------------------
85
+
86
+ def _extract_voicemail_profiles(
87
+ self, result: ExtractionResult
88
+ ) -> list[dict[str, Any]]:
89
+ """List all voicemail profiles, then get full detail for each.
90
+
91
+ Verified against live CUCM 15.0 (2026-03-23):
92
+ - listVoiceMailProfile method confirmed
93
+ - voiceMailPilot reference NOT returned by list — requires getVoiceMailProfile
94
+ """
95
+ profiles: list[dict[str, Any]] = []
96
+ try:
97
+ summaries = self.paginated_list(
98
+ "listVoiceMailProfile",
99
+ {"name": "%"},
100
+ VOICEMAIL_PROFILE_LIST_RETURNED_TAGS,
101
+ )
102
+ except Exception as exc:
103
+ msg = f"listVoiceMailProfile failed: {exc}"
104
+ logger.error("[%s] %s", self.name, msg)
105
+ result.errors.append(msg)
106
+ return profiles
107
+
108
+ for summary in summaries:
109
+ result.total += 1
110
+ profile_name = summary.get("name", "")
111
+ if not profile_name:
112
+ result.failed += 1
113
+ continue
114
+ try:
115
+ detail = self.get_detail("getVoiceMailProfile", name=profile_name)
116
+ except Exception as exc:
117
+ logger.warning(
118
+ "[%s] getVoiceMailProfile error for %s: %s",
119
+ self.name, profile_name, exc,
120
+ )
121
+ profiles.append(summary)
122
+ continue
123
+ if detail is None:
124
+ profiles.append(summary)
125
+ continue
126
+ profiles.append(detail)
127
+
128
+ return profiles
129
+
130
+ # ------------------------------------------------------------------
131
+ # Voicemail Pilots
132
+ # ------------------------------------------------------------------
133
+
134
+ def _extract_voicemail_pilots(
135
+ self, result: ExtractionResult
136
+ ) -> list[dict[str, Any]]:
137
+ """List all voicemail pilots.
138
+
139
+ Verified against live CUCM 15.0 2026-03-23: search field is 'dirn'.
140
+ """
141
+ pilots: list[dict[str, Any]] = []
142
+ try:
143
+ summaries = self.paginated_list(
144
+ "listVoiceMailPilot",
145
+ {"dirn": "%"},
146
+ VOICEMAIL_PILOT_LIST_RETURNED_TAGS,
147
+ )
148
+ pilots.extend(summaries)
149
+ result.total += len(summaries)
150
+ except Exception as exc:
151
+ msg = f"listVoiceMailPilot failed: {exc}"
152
+ logger.error("[%s] %s", self.name, msg)
153
+ result.errors.append(msg)
154
+
155
+ return pilots
156
+
157
+ # ------------------------------------------------------------------
158
+ # Unity Connection shared mailboxes
159
+ # ------------------------------------------------------------------
160
+
161
+ def _extract_shared_mailboxes(
162
+ self, result: ExtractionResult
163
+ ) -> list[dict[str, Any]]:
164
+ """Fetch Unity Connection shared/group call handlers via CUPI.
165
+
166
+ Returns an empty list when no Unity Connection client was
167
+ configured. Errors are logged but do not fail discovery.
168
+ """
169
+ if self.unity_client is None:
170
+ logger.info(
171
+ "[%s] No Unity Connection client — "
172
+ "skipping shared mailbox extraction",
173
+ self.name,
174
+ )
175
+ return []
176
+ try:
177
+ mailboxes = self.unity_client.extract_shared_mailboxes()
178
+ except Exception as exc:
179
+ msg = f"Unity shared mailbox extraction failed: {exc}"
180
+ logger.error("[%s] %s", self.name, msg)
181
+ result.errors.append(msg)
182
+ return []
183
+ result.total += len(mailboxes)
184
+ return mailboxes
185
+
186
+ # ------------------------------------------------------------------
187
+ # Unity Connection per-user settings
188
+ # ------------------------------------------------------------------
189
+
190
+ def extract_unity_settings(
191
+ self, user_ids: list[str]
192
+ ) -> dict[str, dict[str, Any]]:
193
+ """Extract per-user voicemail settings from Unity Connection.
194
+
195
+ This is called separately from ``extract()`` because the user
196
+ list comes from the discovery extractor (02b §2.1), not from
197
+ this extractor.
198
+
199
+ Args:
200
+ user_ids: List of CUCM user IDs (aliases) to look up in
201
+ Unity Connection.
202
+
203
+ Returns:
204
+ Dict mapping CUCM user ID to their Unity Connection
205
+ voicemail settings. Users not found in UC are omitted.
206
+ """
207
+ if self.unity_client is None:
208
+ logger.info(
209
+ "[%s] No Unity Connection client configured — "
210
+ "skipping per-user VM extraction",
211
+ self.name,
212
+ )
213
+ return {}
214
+
215
+ settings: dict[str, dict[str, Any]] = {}
216
+ for user_id in user_ids:
217
+ try:
218
+ user_settings = self.unity_client.extract_user_vm_settings(
219
+ user_id
220
+ )
221
+ if user_settings is not None:
222
+ settings[user_id] = user_settings
223
+ else:
224
+ logger.debug(
225
+ "[%s] User %s not found in Unity Connection",
226
+ self.name,
227
+ user_id,
228
+ )
229
+ except Exception as exc:
230
+ logger.warning(
231
+ "[%s] Unity extraction failed for user %s: %s",
232
+ self.name,
233
+ user_id,
234
+ exc,
235
+ )
236
+
237
+ logger.info(
238
+ "[%s] Extracted Unity settings for %d/%d users",
239
+ self.name,
240
+ len(settings),
241
+ len(user_ids),
242
+ )
243
+ return settings
@@ -0,0 +1,88 @@
1
+ """Workspace (common-area phone) classifier post-processor.
2
+
3
+ Identifies phones that are common-area devices suitable for Webex Calling
4
+ workspace provisioning. Runs as a pass 2 helper against already-extracted
5
+ phone data -- does NOT make AXL calls.
6
+
7
+ Sources:
8
+ - 02b-cucm-extraction.md S2.8 (workspace classification)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+ from wxcli.migration.cucm.extractors.helpers import ref_value
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def is_common_area_device(phone_raw: dict[str, Any]) -> bool:
22
+ """Determine if a phone dict represents a common-area device.
23
+
24
+ Classification logic (from 02b S2.8):
25
+ - Must have no ownerUserName (unowned device).
26
+ - Must have device class 'Phone' (excludes CTI ports, route points, etc.).
27
+
28
+ Args:
29
+ phone_raw: Raw phone dict from DeviceExtractor.
30
+
31
+ Returns:
32
+ True if the phone is a common-area device.
33
+ """
34
+ # ownerUserName may be a reference dict {'_value_1': 'user', 'uuid': '...'}
35
+ # or a plain string. Use ref_value to handle both.
36
+ owner = ref_value(phone_raw.get("ownerUserName"))
37
+ if owner is not None:
38
+ return False
39
+
40
+ device_class = phone_raw.get("class", "")
41
+ if device_class not in ("Phone",):
42
+ return False
43
+
44
+ return True
45
+
46
+
47
+ class WorkspaceClassifier:
48
+ """Classify common-area phones from DeviceExtractor phone data.
49
+
50
+ This is a pass 2 helper, NOT a BaseExtractor subclass. It operates
51
+ entirely on in-memory phone dicts and returns the subset that qualify
52
+ as common-area devices.
53
+
54
+ (from 02b S2.8)
55
+ """
56
+
57
+ def __init__(self, phones: list[dict]) -> None:
58
+ self.phones = phones
59
+
60
+ def classify(self) -> list[dict[str, Any]]:
61
+ """Return the subset of phones that are common-area devices.
62
+
63
+ Returns the raw phone dicts unchanged -- transformation is the
64
+ normalizer's job.
65
+
66
+ Note: ATA devices (name starts with 'ATA') pass the classification
67
+ since they are technically common-area, but may be incompatible with
68
+ Webex Calling workspaces. Downstream stages should flag these.
69
+
70
+ Returns:
71
+ List of raw phone dicts that are common-area devices.
72
+ """
73
+ results = [phone for phone in self.phones if is_common_area_device(phone)]
74
+
75
+ ata_count = sum(1 for p in results if p.get("name", "").startswith("ATA"))
76
+ if ata_count:
77
+ logger.warning(
78
+ "Classified %d ATA device(s) as common-area -- these may be "
79
+ "incompatible with Webex Calling workspaces",
80
+ ata_count,
81
+ )
82
+
83
+ logger.info(
84
+ "Workspace classification complete: %d of %d phones are common-area",
85
+ len(results),
86
+ len(self.phones),
87
+ )
88
+ return results
@@ -0,0 +1,304 @@
1
+ """CUPI REST client for Cisco Unity Connection.
2
+
3
+ Retrieves per-user voicemail settings that are not available via CUCM AXL.
4
+
5
+ Sources:
6
+ - 02b-cucm-extraction.md §2.6 (CUPI connection setup, endpoints, field mapping)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any
13
+
14
+ import requests
15
+ from requests.auth import HTTPBasicAuth
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class UnityConnectionError(Exception):
21
+ """Raised when the Unity Connection client cannot connect or authenticate."""
22
+
23
+
24
+ class UnityConnectionClient:
25
+ """CUPI REST client for Unity Connection per-user voicemail settings.
26
+
27
+ (from 02b §2.6: CUPI connection setup)
28
+
29
+ Args:
30
+ host: Unity Connection hostname or IP.
31
+ username: Admin username.
32
+ password: Admin password.
33
+ verify_ssl: Whether to verify SSL certificates (default False).
34
+ timeout: Request timeout in seconds (default 30).
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ host: str,
40
+ username: str,
41
+ password: str,
42
+ verify_ssl: bool = False,
43
+ timeout: int = 30,
44
+ ) -> None:
45
+ self.base_url = f"https://{host}/vmrest"
46
+ self.session = requests.Session()
47
+ self.session.auth = HTTPBasicAuth(username, password)
48
+ self.session.verify = verify_ssl
49
+ self.session.timeout = timeout
50
+ self.session.headers.update(
51
+ {
52
+ "Accept": "application/json",
53
+ "Content-Type": "application/json",
54
+ }
55
+ )
56
+
57
+ # ------------------------------------------------------------------
58
+ # User lookup
59
+ # ------------------------------------------------------------------
60
+
61
+ def get_user_by_alias(self, alias: str) -> dict[str, Any] | None:
62
+ """Look up UC user by CUCM userid/alias.
63
+
64
+ (from 02b §2.6: CUPI user lookup)
65
+
66
+ """
67
+ try:
68
+ resp = self.session.get(
69
+ f"{self.base_url}/users",
70
+ params={"query": f"(alias is {alias})"},
71
+ )
72
+ if resp.status_code == 200:
73
+ data = resp.json()
74
+ users = data.get("User", [])
75
+ if isinstance(users, dict):
76
+ users = [users]
77
+ return users[0] if users else None
78
+ except Exception as exc:
79
+ logger.warning("CUPI user lookup failed for %s: %s", alias, exc)
80
+ return None
81
+
82
+ # ------------------------------------------------------------------
83
+ # Per-user voicemail settings
84
+ # ------------------------------------------------------------------
85
+
86
+ def get_notification_devices(self, object_id: str) -> list[dict[str, Any]]:
87
+ """Get notification devices for a user.
88
+
89
+ (from 02b §2.6: notifications.enabled, notifications.destination)
90
+
91
+ """
92
+ return self._get_list(
93
+ f"/users/{object_id}/notificationdevices",
94
+ "NotificationDevice",
95
+ )
96
+
97
+ def get_call_handler(self, handler_id: str) -> dict[str, Any] | None:
98
+ """Get call handler (greeting/transfer rules).
99
+
100
+ (from 02b §2.6: sendAllCalls, sendBusyCalls, sendUnansweredCalls)
101
+
102
+ """
103
+ return self._get(f"/handlers/callhandlers/{handler_id}")
104
+
105
+ def get_message_store_settings(
106
+ self, object_id: str
107
+ ) -> dict[str, Any] | None:
108
+ """Get message store settings for a user.
109
+
110
+ (from 02b §2.6: storageType, mwiEnabled, externalEmail)
111
+
112
+ """
113
+ return self._get(f"/users/{object_id}/messagestoresettings")
114
+
115
+ def get_transfer_rule_zero(self, object_id: str) -> dict[str, Any] | None:
116
+ """Get transfer-to-zero rule for a user.
117
+
118
+ (from 02b §2.6: transferToNumber settings)
119
+
120
+ """
121
+ return self._get(f"/users/{object_id}/transferrules/transferzero")
122
+
123
+ def get_fax_settings(self, object_id: str) -> dict[str, Any] | None:
124
+ """Get fax server settings for a user.
125
+
126
+ (from 02b §2.6: fax message settings)
127
+
128
+ """
129
+ return self._get(f"/users/{object_id}/faxserversettings")
130
+
131
+ # ------------------------------------------------------------------
132
+ # Aggregate extraction
133
+ # ------------------------------------------------------------------
134
+
135
+ def extract_user_vm_settings(
136
+ self, cucm_userid: str
137
+ ) -> dict[str, Any] | None:
138
+ """Extract all voicemail settings for one user.
139
+
140
+ Looks up the user by alias, then fetches all per-user settings.
141
+ Returns a consolidated dict, or None if user not found in UC.
142
+
143
+ (from 02b §2.6: extraction flow steps 3-4)
144
+ """
145
+ uc_user = self.get_user_by_alias(cucm_userid)
146
+ if uc_user is None:
147
+ return None
148
+
149
+ object_id = uc_user.get("ObjectId", "")
150
+ call_handler_id = uc_user.get("CallhandlerObjectId", "")
151
+
152
+ result: dict[str, Any] = {
153
+ "uc_object_id": object_id,
154
+ "vm_enabled": True,
155
+ "call_handler_id": call_handler_id,
156
+ }
157
+
158
+ # Notification devices
159
+ notif_devices = self.get_notification_devices(object_id)
160
+ result["notification_enabled"] = any(
161
+ d.get("Enabled", False) for d in notif_devices
162
+ )
163
+ email_devices = [
164
+ d for d in notif_devices if d.get("SmtpAddress")
165
+ ]
166
+ result["notification_destination"] = (
167
+ email_devices[0].get("SmtpAddress") if email_devices else None
168
+ )
169
+
170
+ # Call handler transfer rules
171
+ if call_handler_id:
172
+ handler = self.get_call_handler(call_handler_id)
173
+ if handler:
174
+ result["send_all_calls"] = _to_bool(
175
+ handler.get("PlayTransferPrompt")
176
+ )
177
+ result["send_busy_calls"] = _to_bool(
178
+ handler.get("SendBusyCalls")
179
+ )
180
+ result["send_unanswered_calls"] = _to_bool(
181
+ handler.get("SendUnansweredCalls")
182
+ )
183
+ result["unanswered_rings"] = handler.get("RingsToWait")
184
+ result["busy_greeting_type"] = (
185
+ "CUSTOM"
186
+ if handler.get("PlayPostGreetingRecording")
187
+ else "DEFAULT"
188
+ )
189
+
190
+ # Message store settings
191
+ msg_store = self.get_message_store_settings(object_id)
192
+ if msg_store:
193
+ result["storage_type"] = msg_store.get("StorageType", "INTERNAL")
194
+ result["mwi_enabled"] = _to_bool(msg_store.get("MwiEnabled"))
195
+ result["external_email"] = msg_store.get("ExternalEmail")
196
+ result["email_copy_enabled"] = _to_bool(
197
+ msg_store.get("SendCopyToEmail")
198
+ )
199
+
200
+ # Transfer to zero
201
+ t_zero = self.get_transfer_rule_zero(object_id)
202
+ if t_zero:
203
+ result["transfer_to_zero_enabled"] = _to_bool(
204
+ t_zero.get("Enabled")
205
+ )
206
+ result["transfer_to_zero_destination"] = t_zero.get(
207
+ "Extension"
208
+ ) or t_zero.get("PhoneNumber")
209
+
210
+ # Fax settings
211
+ fax = self.get_fax_settings(object_id)
212
+ if fax:
213
+ result["fax_enabled"] = _to_bool(fax.get("Enabled"))
214
+ result["fax_number"] = fax.get("FaxNumber")
215
+
216
+ return result
217
+
218
+ # ------------------------------------------------------------------
219
+ # Shared/group mailbox discovery
220
+ # ------------------------------------------------------------------
221
+
222
+ def extract_shared_mailboxes(self) -> list[dict[str, Any]]:
223
+ """Extract Unity Connection shared/group call handlers.
224
+
225
+ Queries /vmrest/handlers/callhandlers and filters to non-primary
226
+ handlers (IsPrimary=false). Primary handlers are per-user mailboxes
227
+ already covered by extract_user_vm_settings. Non-primary handlers
228
+ are shared/group mailboxes (sales@, support@, etc.).
229
+
230
+ Returns an empty list on error or when CUPI is unreachable —
231
+ callers should treat absence as "Unity Connection not available"
232
+ rather than "no shared mailboxes exist".
233
+ """
234
+ try:
235
+ resp = self.session.get(
236
+ f"{self.base_url}/handlers/callhandlers",
237
+ )
238
+ except Exception as exc:
239
+ logger.warning("CUPI shared-mailbox query failed: %s", exc)
240
+ return []
241
+
242
+ if resp.status_code != 200:
243
+ logger.warning(
244
+ "CUPI shared-mailbox query returned %s", resp.status_code
245
+ )
246
+ return []
247
+
248
+ data = resp.json() if callable(getattr(resp, "json", None)) else {}
249
+ handlers = data.get("Callhandler", [])
250
+ # CUPI returns a bare dict when exactly one handler exists.
251
+ if isinstance(handlers, dict):
252
+ handlers = [handlers]
253
+
254
+ # Filter out primary (per-user) handlers — we only want shared ones.
255
+ shared: list[dict[str, Any]] = []
256
+ for h in handlers:
257
+ if not isinstance(h, dict):
258
+ continue
259
+ is_primary = str(h.get("IsPrimary", "false")).lower() == "true"
260
+ if is_primary:
261
+ continue
262
+ shared.append(h)
263
+
264
+ logger.info(
265
+ "Extracted %d Unity Connection shared mailbox(es)", len(shared)
266
+ )
267
+ return shared
268
+
269
+ # ------------------------------------------------------------------
270
+ # Internal helpers
271
+ # ------------------------------------------------------------------
272
+
273
+ def _get(self, path: str) -> dict[str, Any] | None:
274
+ """GET a single CUPI resource."""
275
+ try:
276
+ resp = self.session.get(f"{self.base_url}{path}")
277
+ if resp.status_code == 200:
278
+ return resp.json()
279
+ except Exception as exc:
280
+ logger.warning("CUPI GET %s failed: %s", path, exc)
281
+ return None
282
+
283
+ def _get_list(
284
+ self, path: str, key: str
285
+ ) -> list[dict[str, Any]]:
286
+ """GET a CUPI collection resource."""
287
+ data = self._get(path)
288
+ if data is None:
289
+ return []
290
+ items = data.get(key, [])
291
+ if isinstance(items, dict):
292
+ items = [items]
293
+ return items
294
+
295
+
296
+ def _to_bool(value: Any) -> bool | None:
297
+ """Convert CUPI response values to bool."""
298
+ if value is None:
299
+ return None
300
+ if isinstance(value, bool):
301
+ return value
302
+ if isinstance(value, str):
303
+ return value.lower() in ("true", "1", "yes")
304
+ return bool(value)