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,493 @@
1
+ """Routing extractor — partitions, CSS, route patterns, gateways,
2
+ SIP trunks, route groups, route lists, and translation patterns.
3
+
4
+ Covers 8 object types that define CUCM call routing topology.
5
+
6
+ Sources:
7
+ - 02b-cucm-extraction.md §2.4 (routing extraction, all 8 types)
8
+ - 02b-cucm-extraction.md §3 (base extractor, pagination)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+ from wxcli.migration.cucm.connection import AXLConnection
17
+ from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
18
+ from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # ------------------------------------------------------------------
23
+ # ReturnedTags constants — one per AXL object type (from 02b §2.4)
24
+ # ------------------------------------------------------------------
25
+
26
+ PARTITION_RETURNED_TAGS = {'name': '', 'description': ''}
27
+
28
+ # Verified: listCss does NOT accept 'members' — only getCss returns members.
29
+ # List with name+description only, then getCss for full member ordering.
30
+ CSS_LIST_RETURNED_TAGS = {'name': '', 'description': ''}
31
+
32
+ # Verified against live CUCM 15.0 (2026-03-23):
33
+ # - 'destination' is NOT a valid listRoutePattern returnedTag; route targets
34
+ # require getRoutePattern for full resolution
35
+ # - 'networkLocation' and 'prefixDigitsOut' confirmed present in list response
36
+ ROUTE_PATTERN_RETURNED_TAGS = {
37
+ 'pattern': '', 'routePartitionName': '',
38
+ 'blockEnable': '', 'description': '', 'calledPartyTransformationMask': '',
39
+ 'callingPartyTransformationMask': '', 'prefixDigitsOut': '', 'networkLocation': '',
40
+ }
41
+
42
+ # Verified: listGateway does NOT return devicePoolName — only domainName,
43
+ # description, product, protocol. Use getGateway for devicePoolName.
44
+ GATEWAY_RETURNED_TAGS = {
45
+ 'domainName': '', 'description': '', 'product': '', 'protocol': '',
46
+ }
47
+
48
+ # Verified against live CUCM 15.0 (2026-03-23):
49
+ # - listSipTrunk does NOT accept 'destinations' — only getSipTrunk returns those
50
+ # - getSipTrunk returns destinations.destination[].addressIpv4, .port, .sortOrder
51
+ # - sipTrunkType returns values like 'None(Default)'
52
+ SIP_TRUNK_LIST_RETURNED_TAGS = {
53
+ 'name': '', 'description': '', 'devicePoolName': '',
54
+ 'sipProfileName': '', 'securityProfileName': '',
55
+ 'sipTrunkType': '',
56
+ }
57
+
58
+ # Verified: list schemas are restrictive. Use minimal tags for discovery, get for detail.
59
+ ROUTE_GROUP_LIST_RETURNED_TAGS = {'name': ''}
60
+ ROUTE_LIST_LIST_RETURNED_TAGS = {'name': ''}
61
+
62
+ TRANSLATION_PATTERN_RETURNED_TAGS = {
63
+ 'pattern': '', 'calledPartyTransformationMask': '', 'description': '',
64
+ 'routePartitionName': '',
65
+ }
66
+
67
+
68
+ class RoutingExtractor(BaseExtractor):
69
+ """Extract CUCM routing configuration — 8 object types.
70
+
71
+ (from 02b §2.4)
72
+ """
73
+
74
+ name = "routing"
75
+
76
+ def __init__(self, connection: AXLConnection) -> None:
77
+ super().__init__(connection)
78
+ self.results: dict[str, list[dict[str, Any]]] = {}
79
+
80
+ def extract(self) -> ExtractionResult:
81
+ """Extract all 8 routing object types and aggregate results."""
82
+ result = ExtractionResult(extractor=self.name)
83
+
84
+ # Each sub-method populates self.results[key] and returns (count, errors)
85
+ extractors = [
86
+ ("partitions", self._extract_partitions),
87
+ ("css_list", self._extract_css),
88
+ ("route_patterns", self._extract_route_patterns),
89
+ ("gateways", self._extract_gateways),
90
+ ("sip_trunks", self._extract_sip_trunks),
91
+ ("route_groups", self._extract_route_groups),
92
+ ("route_lists", self._extract_route_lists),
93
+ ("translation_patterns", self._extract_translation_patterns),
94
+ ]
95
+
96
+ for key, extract_fn in extractors:
97
+ count, errors = extract_fn()
98
+ result.total += count
99
+ result.failed += len(errors)
100
+ result.errors.extend(errors)
101
+
102
+ logger.info(
103
+ "[%s] Extracted %d total objects (%d failed)",
104
+ self.name, result.success_count, result.failed,
105
+ )
106
+ return result
107
+
108
+ # ------------------------------------------------------------------
109
+ # Partitions — list only (simple objects)
110
+ # ------------------------------------------------------------------
111
+
112
+ def _extract_partitions(self) -> tuple[int, list[str]]:
113
+ logger.info("[%s] Listing partitions...", self.name)
114
+ try:
115
+ partitions = self.paginated_list(
116
+ method_name="listRoutePartition",
117
+ search_criteria={"name": "%"},
118
+ returned_tags=PARTITION_RETURNED_TAGS,
119
+ )
120
+ except Exception as exc:
121
+ msg = f"listRoutePartition failed: {exc}"
122
+ logger.error("[%s] %s", self.name, msg)
123
+ self.results["partitions"] = []
124
+ return 0, [msg]
125
+ self.results["partitions"] = partitions
126
+ logger.info("[%s] Found %d partitions", self.name, len(partitions))
127
+ return len(partitions), []
128
+
129
+ # ------------------------------------------------------------------
130
+ # CSS — list + get for full ordered members (CRITICAL: preserve ordering)
131
+ # ------------------------------------------------------------------
132
+
133
+ def _extract_css(self) -> tuple[int, list[str]]:
134
+ logger.info("[%s] Listing calling search spaces...", self.name)
135
+ try:
136
+ css_summaries = self.paginated_list(
137
+ method_name="listCss",
138
+ search_criteria={"name": "%"},
139
+ returned_tags=CSS_LIST_RETURNED_TAGS,
140
+ )
141
+ except Exception as exc:
142
+ msg = f"listCss failed: {exc}"
143
+ logger.error("[%s] %s", self.name, msg)
144
+ self.results["css_list"] = []
145
+ return 0, [msg]
146
+ logger.info("[%s] Found %d CSS", self.name, len(css_summaries))
147
+
148
+ css_list: list[dict[str, Any]] = []
149
+ errors: list[str] = []
150
+ for summary in css_summaries:
151
+ css_name = ref_value(summary.get("name")) or summary.get("name")
152
+ if not css_name:
153
+ errors.append("CSS with no name in listCss result")
154
+ continue
155
+
156
+ detail = self._get_css_detail(css_name)
157
+ if detail is None:
158
+ errors.append(f"getCss failed for {css_name}")
159
+ continue
160
+
161
+ css_list.append(detail)
162
+
163
+ self.results["css_list"] = css_list
164
+ return len(css_summaries), errors
165
+
166
+ def _get_css_detail(self, css_name: str) -> dict[str, Any] | None:
167
+ """Fetch full CSS detail and normalize member ordering.
168
+
169
+ CRITICAL: CSS member ordering determines partition priority.
170
+ The ``index`` field on each member defines the evaluation order.
171
+ Members are sorted by index to preserve correct ordering.
172
+ """
173
+ try:
174
+ detail = self.get_detail("getCss", name=css_name)
175
+ except Exception as exc:
176
+ logger.warning("[%s] getCss error for %s: %s", self.name, css_name, exc)
177
+ return None
178
+
179
+ if detail is None:
180
+ return None
181
+
182
+ # Normalize members — may be None, empty dict, or populated
183
+ raw_members = detail.get("members")
184
+ member_entries = to_list(raw_members, "member")
185
+
186
+ # Sort by index to preserve partition evaluation order
187
+ sorted_members = sorted(
188
+ member_entries,
189
+ key=lambda m: int(m.get("index", 0)) if isinstance(m, dict) else 0,
190
+ )
191
+
192
+ detail["members"] = sorted_members
193
+ return detail
194
+
195
+ # ------------------------------------------------------------------
196
+ # Route Patterns — list + get (destination requires getRoutePattern)
197
+ # Verified: listRoutePattern does NOT return destination/gateway/routeList
198
+ # fields. getRoutePattern returns the full routing target.
199
+ # ------------------------------------------------------------------
200
+
201
+ def _extract_route_patterns(self) -> tuple[int, list[str]]:
202
+ logger.info("[%s] Listing route patterns...", self.name)
203
+ try:
204
+ rp_summaries = self.paginated_list(
205
+ method_name="listRoutePattern",
206
+ search_criteria={"pattern": "%"},
207
+ returned_tags=ROUTE_PATTERN_RETURNED_TAGS,
208
+ )
209
+ except Exception as exc:
210
+ msg = f"listRoutePattern failed: {exc}"
211
+ logger.error("[%s] %s", self.name, msg)
212
+ self.results["route_patterns"] = []
213
+ return 0, [msg]
214
+ logger.info("[%s] Found %d route patterns", self.name, len(rp_summaries))
215
+
216
+ patterns: list[dict[str, Any]] = []
217
+ errors: list[str] = []
218
+ for summary in rp_summaries:
219
+ rp_pattern = summary.get("pattern", "")
220
+ rp_partition = ref_value(summary.get("routePartitionName")) or summary.get("routePartitionName")
221
+ try:
222
+ detail = self.get_detail(
223
+ "getRoutePattern",
224
+ pattern=rp_pattern,
225
+ routePartitionName=rp_partition or "",
226
+ )
227
+ except Exception as exc:
228
+ logger.warning(
229
+ "[%s] getRoutePattern error for %s:%s: %s",
230
+ self.name, rp_pattern, rp_partition, exc,
231
+ )
232
+ errors.append(f"getRoutePattern failed for {rp_pattern}:{rp_partition}")
233
+ patterns.append(summary)
234
+ continue
235
+ if detail is None:
236
+ patterns.append(summary)
237
+ continue
238
+ patterns.append(detail)
239
+
240
+ self.results["route_patterns"] = patterns
241
+ return len(rp_summaries), errors
242
+
243
+ # ------------------------------------------------------------------
244
+ # Gateways — list + get (devicePoolName requires getGateway)
245
+ # Verified: listGateway search criteria is domainName
246
+ # ------------------------------------------------------------------
247
+
248
+ def _extract_gateways(self) -> tuple[int, list[str]]:
249
+ logger.info("[%s] Listing gateways...", self.name)
250
+ try:
251
+ gw_summaries = self.paginated_list(
252
+ method_name="listGateway",
253
+ search_criteria={"domainName": "%"},
254
+ returned_tags=GATEWAY_RETURNED_TAGS,
255
+ )
256
+ except Exception as exc:
257
+ msg = f"listGateway failed: {exc}"
258
+ logger.error("[%s] %s", self.name, msg)
259
+ self.results["gateways"] = []
260
+ return 0, [msg]
261
+ logger.info("[%s] Found %d gateways", self.name, len(gw_summaries))
262
+
263
+ gateways: list[dict[str, Any]] = []
264
+ errors: list[str] = []
265
+ for summary in gw_summaries:
266
+ gw_name = ref_value(summary.get("domainName")) or summary.get("domainName")
267
+ if not gw_name:
268
+ errors.append("Gateway with no domainName in listGateway result")
269
+ continue
270
+ try:
271
+ detail = self.get_detail("getGateway", domainName=gw_name)
272
+ except Exception as exc:
273
+ logger.warning("[%s] getGateway error for %s: %s", self.name, gw_name, exc)
274
+ errors.append(f"getGateway failed for {gw_name}")
275
+ # Fall back to summary (no devicePoolName)
276
+ gateways.append(summary)
277
+ continue
278
+ if detail is None:
279
+ gateways.append(summary)
280
+ continue
281
+ gateways.append(detail)
282
+
283
+ self.results["gateways"] = gateways
284
+ return len(gw_summaries), errors
285
+
286
+ # ------------------------------------------------------------------
287
+ # SIP Trunks — list + get (destinations require getSipTrunk)
288
+ # Verified: listSipTrunk method confirmed on live CUCM 15.0
289
+ # ------------------------------------------------------------------
290
+
291
+ def _extract_sip_trunks(self) -> tuple[int, list[str]]:
292
+ logger.info("[%s] Listing SIP trunks...", self.name)
293
+ try:
294
+ trunk_summaries = self.paginated_list(
295
+ method_name="listSipTrunk",
296
+ search_criteria={"name": "%"},
297
+ returned_tags=SIP_TRUNK_LIST_RETURNED_TAGS,
298
+ )
299
+ except Exception as exc:
300
+ msg = f"listSipTrunk failed: {exc}"
301
+ logger.error("[%s] %s", self.name, msg)
302
+ self.results["sip_trunks"] = []
303
+ return 0, [msg]
304
+ logger.info("[%s] Found %d SIP trunks", self.name, len(trunk_summaries))
305
+
306
+ # getSipTrunk for full detail including destinations array
307
+ trunks: list[dict[str, Any]] = []
308
+ errors: list[str] = []
309
+ for summary in trunk_summaries:
310
+ trunk_name = ref_value(summary.get("name")) or summary.get("name")
311
+ if not trunk_name:
312
+ errors.append("SIP trunk with no name in listSipTrunk result")
313
+ continue
314
+ try:
315
+ detail = self.get_detail("getSipTrunk", name=trunk_name)
316
+ except Exception as exc:
317
+ logger.warning("[%s] getSipTrunk error for %s: %s", self.name, trunk_name, exc)
318
+ errors.append(f"getSipTrunk failed for {trunk_name}")
319
+ trunks.append(summary)
320
+ continue
321
+ if detail is None:
322
+ trunks.append(summary)
323
+ continue
324
+ # Normalize destinations list
325
+ raw_dest = detail.get("destinations") if hasattr(detail, "get") else None
326
+ if raw_dest:
327
+ detail["destinations"] = to_list(raw_dest, "destination")
328
+ trunks.append(detail)
329
+
330
+ # Second pass: fetch SIP profile and security profile detail
331
+ # Collect unique profile names across all trunks
332
+ sip_profile_names: set[str] = set()
333
+ security_profile_names: set[str] = set()
334
+ for trunk in trunks:
335
+ sip_name = ref_value(trunk.get("sipProfileName")) or trunk.get("sipProfileName")
336
+ if sip_name and isinstance(sip_name, str):
337
+ sip_profile_names.add(sip_name)
338
+ sec_name = ref_value(trunk.get("securityProfileName")) or trunk.get("securityProfileName")
339
+ if sec_name and isinstance(sec_name, str):
340
+ security_profile_names.add(sec_name)
341
+
342
+ # Fetch SIP profiles (cache to avoid duplicate calls)
343
+ sip_profile_cache: dict[str, dict[str, Any]] = {}
344
+ for profile_name in sip_profile_names:
345
+ try:
346
+ detail = self.get_detail("getSipProfile", name=profile_name)
347
+ if detail is not None:
348
+ sip_profile_cache[profile_name] = detail
349
+ except Exception as exc:
350
+ logger.warning(
351
+ "[%s] getSipProfile error for %s: %s", self.name, profile_name, exc,
352
+ )
353
+
354
+ # Fetch security profiles (cache to avoid duplicate calls)
355
+ security_profile_cache: dict[str, dict[str, Any]] = {}
356
+ for profile_name in security_profile_names:
357
+ try:
358
+ detail = self.get_detail("getSipTrunkSecurityProfile", name=profile_name)
359
+ if detail is not None:
360
+ security_profile_cache[profile_name] = detail
361
+ except Exception as exc:
362
+ logger.warning(
363
+ "[%s] getSipTrunkSecurityProfile error for %s: %s", self.name, profile_name, exc,
364
+ )
365
+
366
+ # Merge profile detail into each trunk dict
367
+ for trunk in trunks:
368
+ sip_name = ref_value(trunk.get("sipProfileName")) or trunk.get("sipProfileName")
369
+ if sip_name and sip_name in sip_profile_cache:
370
+ trunk["_sip_profile_detail"] = sip_profile_cache[sip_name]
371
+ sec_name = ref_value(trunk.get("securityProfileName")) or trunk.get("securityProfileName")
372
+ if sec_name and sec_name in security_profile_cache:
373
+ trunk["_security_profile_detail"] = security_profile_cache[sec_name]
374
+
375
+ self.results["sip_trunks"] = trunks
376
+ return len(trunk_summaries), errors
377
+
378
+ # ------------------------------------------------------------------
379
+ # Route Groups — list + get for member trunks
380
+ # ------------------------------------------------------------------
381
+
382
+ def _extract_route_groups(self) -> tuple[int, list[str]]:
383
+ logger.info("[%s] Listing route groups...", self.name)
384
+ try:
385
+ rg_summaries = self.paginated_list(
386
+ method_name="listRouteGroup",
387
+ search_criteria={"name": "%"},
388
+ returned_tags=ROUTE_GROUP_LIST_RETURNED_TAGS,
389
+ )
390
+ except Exception as exc:
391
+ msg = f"listRouteGroup failed: {exc}"
392
+ logger.error("[%s] %s", self.name, msg)
393
+ self.results["route_groups"] = []
394
+ return 0, [msg]
395
+ logger.info("[%s] Found %d route groups", self.name, len(rg_summaries))
396
+
397
+ route_groups: list[dict[str, Any]] = []
398
+ errors: list[str] = []
399
+ for summary in rg_summaries:
400
+ rg_name = ref_value(summary.get("name")) or summary.get("name")
401
+ if not rg_name:
402
+ errors.append("Route group with no name in listRouteGroup result")
403
+ continue
404
+
405
+ try:
406
+ detail = self.get_detail("getRouteGroup", name=rg_name)
407
+ except Exception as exc:
408
+ logger.warning(
409
+ "[%s] getRouteGroup error for %s: %s", self.name, rg_name, exc,
410
+ )
411
+ errors.append(f"getRouteGroup failed for {rg_name}")
412
+ continue
413
+
414
+ if detail is None:
415
+ errors.append(f"getRouteGroup returned None for {rg_name}")
416
+ continue
417
+
418
+ # Normalize members
419
+ raw_members = detail.get("members")
420
+ detail["members"] = to_list(raw_members, "member")
421
+ route_groups.append(detail)
422
+
423
+ self.results["route_groups"] = route_groups
424
+ return len(rg_summaries), errors
425
+
426
+ # ------------------------------------------------------------------
427
+ # Route Lists — list + get for member route groups
428
+ # ------------------------------------------------------------------
429
+
430
+ def _extract_route_lists(self) -> tuple[int, list[str]]:
431
+ logger.info("[%s] Listing route lists...", self.name)
432
+ try:
433
+ rl_summaries = self.paginated_list(
434
+ method_name="listRouteList",
435
+ search_criteria={"name": "%"},
436
+ returned_tags=ROUTE_LIST_LIST_RETURNED_TAGS,
437
+ )
438
+ except Exception as exc:
439
+ msg = f"listRouteList failed: {exc}"
440
+ logger.error("[%s] %s", self.name, msg)
441
+ self.results["route_lists"] = []
442
+ return 0, [msg]
443
+ logger.info("[%s] Found %d route lists", self.name, len(rl_summaries))
444
+
445
+ route_lists: list[dict[str, Any]] = []
446
+ errors: list[str] = []
447
+ for summary in rl_summaries:
448
+ rl_name = ref_value(summary.get("name")) or summary.get("name")
449
+ if not rl_name:
450
+ errors.append("Route list with no name in listRouteList result")
451
+ continue
452
+
453
+ try:
454
+ detail = self.get_detail("getRouteList", name=rl_name)
455
+ except Exception as exc:
456
+ logger.warning(
457
+ "[%s] getRouteList error for %s: %s", self.name, rl_name, exc,
458
+ )
459
+ errors.append(f"getRouteList failed for {rl_name}")
460
+ continue
461
+
462
+ if detail is None:
463
+ errors.append(f"getRouteList returned None for {rl_name}")
464
+ continue
465
+
466
+ # Normalize members
467
+ raw_members = detail.get("members")
468
+ detail["members"] = to_list(raw_members, "member")
469
+ route_lists.append(detail)
470
+
471
+ self.results["route_lists"] = route_lists
472
+ return len(rl_summaries), errors
473
+
474
+ # ------------------------------------------------------------------
475
+ # Translation Patterns — list only (no get needed)
476
+ # ------------------------------------------------------------------
477
+
478
+ def _extract_translation_patterns(self) -> tuple[int, list[str]]:
479
+ logger.info("[%s] Listing translation patterns...", self.name)
480
+ try:
481
+ patterns = self.paginated_list(
482
+ method_name="listTransPattern",
483
+ search_criteria={"pattern": "%"},
484
+ returned_tags=TRANSLATION_PATTERN_RETURNED_TAGS,
485
+ )
486
+ except Exception as exc:
487
+ msg = f"listTransPattern failed: {exc}"
488
+ logger.error("[%s] %s", self.name, msg)
489
+ self.results["translation_patterns"] = []
490
+ return 0, [msg]
491
+ self.results["translation_patterns"] = patterns
492
+ logger.info("[%s] Found %d translation patterns", self.name, len(patterns))
493
+ return len(patterns), []
@@ -0,0 +1,146 @@
1
+ """Shared line detection post-processor.
2
+
3
+ Identifies directory numbers that appear on two or more devices, indicating
4
+ shared-line appearances. 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.7 (shared line detection)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from collections import defaultdict
15
+ from typing import Any
16
+
17
+ from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class SharedLineDetector:
23
+ """Detect shared lines from DeviceExtractor phone data.
24
+
25
+ This is a pass 2 helper, NOT a BaseExtractor subclass. It operates
26
+ entirely on in-memory phone dicts and produces shared line records.
27
+
28
+ (from 02b S2.7)
29
+ """
30
+
31
+ def __init__(self, phones: list[dict]) -> None:
32
+ self.phones = phones
33
+
34
+ def detect(self) -> list[dict[str, Any]]:
35
+ """Identify all shared lines and return structured dicts.
36
+
37
+ Algorithm:
38
+ 1. Walk every phone's line appearances.
39
+ 2. Group by (dn_pattern, partition).
40
+ 3. Any DN appearing on 2+ devices is shared.
41
+ 4. Build a shared-line dict for each.
42
+
43
+ Returns:
44
+ List of shared line dicts with canonical_id, devices, and
45
+ primary_owner information.
46
+ """
47
+ # Map (dn_pattern, partition) -> list of appearance records
48
+ dn_appearances: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
49
+
50
+ for phone in self.phones:
51
+ device_name = phone.get("name", "")
52
+ owner = ref_value(phone.get("ownerUserName"))
53
+
54
+ lines_container = phone.get("lines")
55
+ if not lines_container:
56
+ continue
57
+
58
+ line_entries = to_list(lines_container, "line")
59
+
60
+ for line_entry in line_entries:
61
+ # Speed dials have no dirn — skip them
62
+ dirn = line_entry.get("dirn")
63
+ if dirn is None:
64
+ continue
65
+
66
+ dn_pattern = ref_value(dirn.get("pattern")) if isinstance(dirn, dict) else None
67
+ if not dn_pattern:
68
+ continue
69
+
70
+ partition = ""
71
+ route_partition = dirn.get("routePartitionName") if isinstance(dirn, dict) else None
72
+ if route_partition is not None:
73
+ partition = ref_value(route_partition) or ""
74
+
75
+ line_index_raw = line_entry.get("index")
76
+ try:
77
+ line_index = int(line_index_raw) if line_index_raw is not None else None
78
+ except (ValueError, TypeError):
79
+ line_index = None
80
+
81
+ # Collect associated endusers for this line appearance
82
+ assoc_endusers_container = line_entry.get("associatedEndusers")
83
+ assoc_endusers: list[str] = []
84
+ if assoc_endusers_container is not None:
85
+ enduser_entries = to_list(assoc_endusers_container, "enduser")
86
+ for eu in enduser_entries:
87
+ eu_id = ref_value(eu.get("userId")) if isinstance(eu, dict) else ref_value(eu)
88
+ if eu_id:
89
+ assoc_endusers.append(eu_id)
90
+
91
+ dn_appearances[(dn_pattern, partition)].append(
92
+ {
93
+ "device_name": device_name,
94
+ "device_owner": owner,
95
+ "line_index": line_index,
96
+ "associated_endusers": assoc_endusers,
97
+ }
98
+ )
99
+
100
+ # Filter to DNs on 2+ devices and build output
101
+ results: list[dict[str, Any]] = []
102
+
103
+ for (dn_pattern, partition), appearances in sorted(dn_appearances.items()):
104
+ if len(appearances) < 2:
105
+ continue
106
+
107
+ # Determine primary owner: user with DN at line index 1.
108
+ # If multiple at index 1, first one wins.
109
+ primary_owner: str | None = None
110
+ for app in appearances:
111
+ if app["line_index"] == 1 and app["device_owner"] is not None:
112
+ primary_owner = app["device_owner"]
113
+ break
114
+
115
+ # Aggregate all associated endusers across devices (deduplicated)
116
+ all_endusers: list[str] = []
117
+ seen_endusers: set[str] = set()
118
+ for app in appearances:
119
+ for eu in app["associated_endusers"]:
120
+ if eu not in seen_endusers:
121
+ seen_endusers.add(eu)
122
+ all_endusers.append(eu)
123
+
124
+ devices = [
125
+ {
126
+ "device_name": app["device_name"],
127
+ "device_owner": app["device_owner"],
128
+ "line_index": app["line_index"],
129
+ }
130
+ for app in appearances
131
+ ]
132
+
133
+ results.append(
134
+ {
135
+ "canonical_id": f"shared_line:{dn_pattern}:{partition}",
136
+ "dn": dn_pattern,
137
+ "partition": partition,
138
+ "device_count": len(appearances),
139
+ "devices": devices,
140
+ "primary_owner": primary_owner,
141
+ "associated_endusers": all_endusers,
142
+ }
143
+ )
144
+
145
+ logger.info("Shared line detection complete: %d shared DNs found", len(results))
146
+ return results