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,347 @@
1
+ import json
2
+ import typer
3
+ from wxcli.auth import get_api
4
+ from wxcli.errors import WebexError, handle_rest_error
5
+ from wxcli.output import print_table, print_json
6
+
7
+
8
+ app = typer.Typer(help="Manage Webex Calling converged-recordings.")
9
+
10
+
11
+ @app.command("list")
12
+ def cmd_list(
13
+ from_param: str = typer.Option(None, "--from", help="Starting date and time (inclusive) for recordings to return,"),
14
+ to: str = typer.Option(None, "--to", help="Ending date and time (exclusive) for List recordings to retu"),
15
+ status: str = typer.Option(None, "--status", help="Choices: available, deleted"),
16
+ service_type: str = typer.Option(None, "--service-type", help="Choices: calling, customerAssist"),
17
+ format_param: str = typer.Option(None, "--format", help="Choices: MP3"),
18
+ owner_type: str = typer.Option(None, "--owner-type", help="Choices: user, place, virtualLine, callQueue"),
19
+ storage_region: str = typer.Option(None, "--storage-region", help="Choices: US, SG, GB, JP, DE, AU, IN, CA"),
20
+ location_id: str = typer.Option(None, "--location-id", help="Fetch recordings for users in a particular Webex Calling loc"),
21
+ topic: str = typer.Option(None, "--topic", help="Recording's topic. If specified, the API filters recordings"),
22
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
23
+ limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
24
+ offset: int = typer.Option(0, "--offset", help="Start offset"),
25
+ debug: bool = typer.Option(False, "--debug"),
26
+ ):
27
+ """List Recordings."""
28
+ api = get_api(debug=debug)
29
+ url = f"https://webexapis.com/v1/convergedRecordings"
30
+ params = {}
31
+ if from_param is not None:
32
+ params["from"] = from_param
33
+ if to is not None:
34
+ params["to"] = to
35
+ if status is not None:
36
+ params["status"] = status
37
+ if service_type is not None:
38
+ params["serviceType"] = service_type
39
+ if format_param is not None:
40
+ params["format"] = format_param
41
+ if owner_type is not None:
42
+ params["ownerType"] = owner_type
43
+ if storage_region is not None:
44
+ params["storageRegion"] = storage_region
45
+ if location_id is not None:
46
+ params["locationId"] = location_id
47
+ if topic is not None:
48
+ params["topic"] = topic
49
+ if limit > 0:
50
+ params["max"] = limit
51
+ if offset > 0:
52
+ params["start"] = offset
53
+ try:
54
+ if limit > 0:
55
+ result = api.session.rest_get(url, params=params)
56
+ result = result or {}
57
+ items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
58
+ else:
59
+ items = list(api.session.follow_pagination(url=url, params=params, item_key="items"))
60
+ except WebexError as e:
61
+ handle_rest_error(e)
62
+ if output == "json":
63
+ print_json(items)
64
+ else:
65
+ print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
66
+
67
+
68
+
69
+ @app.command("list-converged-recordings")
70
+ def list_converged_recordings(
71
+ from_param: str = typer.Option(None, "--from", help="Starting date and time (inclusive) for recordings to return,"),
72
+ to: str = typer.Option(None, "--to", help="Ending date and time (exclusive) for List recordings to retu"),
73
+ status: str = typer.Option(None, "--status", help="Choices: available, deleted, purged"),
74
+ service_type: str = typer.Option(None, "--service-type", help="Choices: calling, customerAssist"),
75
+ format_param: str = typer.Option(None, "--format", help="Choices: MP3"),
76
+ owner_id: str = typer.Option(None, "--owner-id", help="Webex user Id to fetch recordings for a particular user."),
77
+ owner_email: str = typer.Option(None, "--owner-email", help="Webex email address to fetch recordings for a particular use"),
78
+ owner_type: str = typer.Option(None, "--owner-type", help="Choices: user, place, virtualLine, callQueue"),
79
+ storage_region: str = typer.Option(None, "--storage-region", help="Choices: US, SG, GB, JP, DE, AU, IN, CA"),
80
+ location_id: str = typer.Option(None, "--location-id", help="Fetch recordings for users in a particular Webex Calling loc"),
81
+ topic: str = typer.Option(None, "--topic", help="Recording's topic. If specified, the API filters recordings"),
82
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
83
+ limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
84
+ offset: int = typer.Option(0, "--offset", help="Start offset"),
85
+ debug: bool = typer.Option(False, "--debug"),
86
+ ):
87
+ """List Recordings for Admin or Compliance officer."""
88
+ api = get_api(debug=debug)
89
+ url = f"https://webexapis.com/v1/admin/convergedRecordings"
90
+ params = {}
91
+ if from_param is not None:
92
+ params["from"] = from_param
93
+ if to is not None:
94
+ params["to"] = to
95
+ if status is not None:
96
+ params["status"] = status
97
+ if service_type is not None:
98
+ params["serviceType"] = service_type
99
+ if format_param is not None:
100
+ params["format"] = format_param
101
+ if owner_id is not None:
102
+ params["ownerId"] = owner_id
103
+ if owner_email is not None:
104
+ params["ownerEmail"] = owner_email
105
+ if owner_type is not None:
106
+ params["ownerType"] = owner_type
107
+ if storage_region is not None:
108
+ params["storageRegion"] = storage_region
109
+ if location_id is not None:
110
+ params["locationId"] = location_id
111
+ if topic is not None:
112
+ params["topic"] = topic
113
+ if limit > 0:
114
+ params["max"] = limit
115
+ if offset > 0:
116
+ params["start"] = offset
117
+ try:
118
+ if limit > 0:
119
+ result = api.session.rest_get(url, params=params)
120
+ result = result or {}
121
+ items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
122
+ else:
123
+ items = list(api.session.follow_pagination(url=url, params=params, item_key="items"))
124
+ except WebexError as e:
125
+ handle_rest_error(e)
126
+ if output == "json":
127
+ print_json(items)
128
+ else:
129
+ print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
130
+
131
+
132
+
133
+ @app.command("show")
134
+ def show(
135
+ recording_id: str = typer.Argument(help="recordingId"),
136
+ output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
137
+ debug: bool = typer.Option(False, "--debug"),
138
+ ):
139
+ """Get Recording Details."""
140
+ api = get_api(debug=debug)
141
+ url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}"
142
+ try:
143
+ result = api.session.rest_get(url)
144
+ except WebexError as e:
145
+ handle_rest_error(e)
146
+ if output == "json":
147
+ print_json(result)
148
+ else:
149
+ if isinstance(result, dict):
150
+ print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
151
+ elif isinstance(result, list):
152
+ print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
153
+ else:
154
+ print_json(result)
155
+
156
+
157
+
158
+ @app.command("delete")
159
+ def delete(
160
+ recording_id: str = typer.Argument(help="recordingId"),
161
+ force: bool = typer.Option(False, "--force", help="Skip confirmation"),
162
+ debug: bool = typer.Option(False, "--debug"),
163
+ ):
164
+ """Delete a Recording."""
165
+ if not force:
166
+ typer.confirm(f"Delete {recording_id}?", abort=True)
167
+ api = get_api(debug=debug)
168
+ url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}"
169
+ try:
170
+ api.session.rest_delete(url)
171
+ except WebexError as e:
172
+ handle_rest_error(e)
173
+ typer.echo(f"Deleted: {recording_id}")
174
+
175
+
176
+
177
+ @app.command("show-metadata")
178
+ def show_metadata(
179
+ recording_id: str = typer.Argument(help="recordingId"),
180
+ show_all_types: str = typer.Option(None, "--show-all-types", help="If `showAllTypes` is `true`, all attributes will be shown. I"),
181
+ output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
182
+ debug: bool = typer.Option(False, "--debug"),
183
+ ):
184
+ """Get Recording metadata."""
185
+ api = get_api(debug=debug)
186
+ url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}/metadata"
187
+ params = {}
188
+ if show_all_types is not None:
189
+ params["showAllTypes"] = show_all_types
190
+ try:
191
+ result = api.session.rest_get(url, params=params)
192
+ except WebexError as e:
193
+ handle_rest_error(e)
194
+ if output == "json":
195
+ print_json(result)
196
+ else:
197
+ if isinstance(result, dict):
198
+ print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
199
+ elif isinstance(result, list):
200
+ print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
201
+ else:
202
+ print_json(result)
203
+
204
+
205
+
206
+ @app.command("create")
207
+ def create(
208
+ owner_email: str = typer.Option(None, "--owner-email", help="Recording owner email."),
209
+ owner_id: str = typer.Option(None, "--owner-id", help="Recording owner ID. Can be a user, a virtual line, or a work"),
210
+ reassign_owner_email: str = typer.Option(None, "--reassign-owner-email", help="(required) New owner of the recordings."),
211
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
212
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
213
+ debug: bool = typer.Option(False, "--debug"),
214
+ ):
215
+ """Reassign Recordings\n\nExample --json-body:\n '{"reassignOwnerEmail":"...","ownerEmail":"...","ownerID":"...","recordingIds":["..."]}'."""
216
+ api = get_api(debug=debug)
217
+ url = f"https://webexapis.com/v1/convergedRecordings/reassign"
218
+ if json_body:
219
+ body = json.loads(json_body)
220
+ else:
221
+ body = {}
222
+ if owner_email is not None:
223
+ body["ownerEmail"] = owner_email
224
+ if owner_id is not None:
225
+ body["ownerID"] = owner_id
226
+ if reassign_owner_email is not None:
227
+ body["reassignOwnerEmail"] = reassign_owner_email
228
+ _missing = [f for f in ['reassignOwnerEmail'] if f not in body or body[f] is None]
229
+ if _missing:
230
+ typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
231
+ raise typer.Exit(1)
232
+ try:
233
+ result = api.session.rest_post(url, json=body)
234
+ except WebexError as e:
235
+ handle_rest_error(e)
236
+ if output == "json":
237
+ print_json(result)
238
+ elif isinstance(result, dict) and "id" in result:
239
+ typer.echo(f"Created: {result['id']}")
240
+ elif not result or result == {}:
241
+ typer.echo("Created.")
242
+ else:
243
+ print_json(result)
244
+
245
+
246
+
247
+ @app.command("create-soft-delete")
248
+ def create_soft_delete(
249
+ trash_all: bool = typer.Option(None, "--trash-all/--no-trash-all", help="If not specified or `false`, moves the recordings specified"),
250
+ owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
251
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
252
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
253
+ debug: bool = typer.Option(False, "--debug"),
254
+ ):
255
+ """Move Recordings into the Recycle Bin\n\nExample --json-body:\n '{"trashAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
256
+ api = get_api(debug=debug)
257
+ url = f"https://webexapis.com/v1/convergedRecordings/softDelete"
258
+ if json_body:
259
+ body = json.loads(json_body)
260
+ else:
261
+ body = {}
262
+ if trash_all is not None:
263
+ body["trashAll"] = trash_all
264
+ if owner_email is not None:
265
+ body["ownerEmail"] = owner_email
266
+ try:
267
+ result = api.session.rest_post(url, json=body)
268
+ except WebexError as e:
269
+ handle_rest_error(e)
270
+ if output == "json":
271
+ print_json(result)
272
+ elif isinstance(result, dict) and "id" in result:
273
+ typer.echo(f"Created: {result['id']}")
274
+ elif not result or result == {}:
275
+ typer.echo("Created.")
276
+ else:
277
+ print_json(result)
278
+
279
+
280
+
281
+ @app.command("create-restore")
282
+ def create_restore(
283
+ restore_all: bool = typer.Option(None, "--restore-all/--no-restore-all", help="If not specified or `false`, restores the recordings specifi"),
284
+ owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
285
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
286
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
287
+ debug: bool = typer.Option(False, "--debug"),
288
+ ):
289
+ """Restore Recordings from Recycle Bin\n\nExample --json-body:\n '{"restoreAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
290
+ api = get_api(debug=debug)
291
+ url = f"https://webexapis.com/v1/convergedRecordings/restore"
292
+ if json_body:
293
+ body = json.loads(json_body)
294
+ else:
295
+ body = {}
296
+ if restore_all is not None:
297
+ body["restoreAll"] = restore_all
298
+ if owner_email is not None:
299
+ body["ownerEmail"] = owner_email
300
+ try:
301
+ result = api.session.rest_post(url, json=body)
302
+ except WebexError as e:
303
+ handle_rest_error(e)
304
+ if output == "json":
305
+ print_json(result)
306
+ elif isinstance(result, dict) and "id" in result:
307
+ typer.echo(f"Created: {result['id']}")
308
+ elif not result or result == {}:
309
+ typer.echo("Created.")
310
+ else:
311
+ print_json(result)
312
+
313
+
314
+
315
+ @app.command("create-purge")
316
+ def create_purge(
317
+ purge_all: bool = typer.Option(None, "--purge-all/--no-purge-all", help="If not specified or `false`, purges the recordings specified"),
318
+ owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
319
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
320
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
321
+ debug: bool = typer.Option(False, "--debug"),
322
+ ):
323
+ """Purge Recordings from Recycle Bin\n\nExample --json-body:\n '{"purgeAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
324
+ api = get_api(debug=debug)
325
+ url = f"https://webexapis.com/v1/convergedRecordings/purge"
326
+ if json_body:
327
+ body = json.loads(json_body)
328
+ else:
329
+ body = {}
330
+ if purge_all is not None:
331
+ body["purgeAll"] = purge_all
332
+ if owner_email is not None:
333
+ body["ownerEmail"] = owner_email
334
+ try:
335
+ result = api.session.rest_post(url, json=body)
336
+ except WebexError as e:
337
+ handle_rest_error(e)
338
+ if output == "json":
339
+ print_json(result)
340
+ elif isinstance(result, dict) and "id" in result:
341
+ typer.echo(f"Created: {result['id']}")
342
+ elif not result or result == {}:
343
+ typer.echo("Created.")
344
+ else:
345
+ print_json(result)
346
+
347
+
@@ -0,0 +1,310 @@
1
+ """Download and bulk-export converged recording artifacts (audio, transcript, AI notes)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+
9
+ import requests
10
+ import typer
11
+ from rich.console import Console
12
+ from wxcli.errors import WebexError
13
+
14
+ from wxcli.auth import get_api
15
+ from wxcli.errors import handle_rest_error
16
+
17
+ logger = logging.getLogger(__name__)
18
+ console = Console()
19
+
20
+ BASE = "https://webexapis.com/v1"
21
+
22
+ # Mapping from temporaryDirectDownloadLinks field → output filename
23
+ ARTIFACT_MAP = {
24
+ "transcriptDownloadLink": ("transcript.txt", "transcript", False),
25
+ "suggestedNotesDownloadLink": ("suggested_notes.html", "suggestedNotes", False),
26
+ "shortNotesDownloadLink": ("short_notes.html", "shortNotes", False),
27
+ "actionItemsDownloadLink": ("action_items.html", "actionItems", False),
28
+ "audioDownloadLink": ("audio.mp3", "audioFile", True),
29
+ }
30
+
31
+
32
+ @dataclass
33
+ class DownloadResult:
34
+ recording_id: str
35
+ metadata: dict
36
+ downloaded: list[str] = field(default_factory=list)
37
+ unavailable: list[str] = field(default_factory=list)
38
+ failed: list[str] = field(default_factory=list)
39
+ audio_path: str | None = None
40
+
41
+
42
+ def download_recording_artifacts(
43
+ api,
44
+ recording_id: str,
45
+ output_dir: Path,
46
+ include_audio: bool = False,
47
+ ) -> DownloadResult:
48
+ """Fetch recording detail and download all available artifacts."""
49
+ url = f"{BASE}/convergedRecordings/{recording_id}"
50
+ detail = api.session.rest_get(url)
51
+
52
+ result = DownloadResult(recording_id=recording_id, metadata=detail)
53
+
54
+ # Write metadata.json
55
+ output_dir.mkdir(parents=True, exist_ok=True)
56
+ metadata_path = output_dir / "metadata.json"
57
+ metadata_path.write_text(json.dumps(detail, indent=2, default=str))
58
+
59
+ links = detail.get("temporaryDirectDownloadLinks") or {}
60
+
61
+ for link_field, (filename, _content_key, is_audio) in ARTIFACT_MAP.items():
62
+ if is_audio and not include_audio:
63
+ continue
64
+
65
+ download_url = links.get(link_field)
66
+ if not download_url:
67
+ result.unavailable.append(filename)
68
+ continue
69
+
70
+ try:
71
+ if is_audio:
72
+ resp = requests.get(download_url, timeout=300, stream=True)
73
+ resp.raise_for_status()
74
+ else:
75
+ resp = requests.get(download_url, timeout=300)
76
+ resp.raise_for_status()
77
+ except Exception as e:
78
+ logger.warning("Failed to download %s for %s: %s", filename, recording_id, e)
79
+ result.failed.append(filename)
80
+ continue
81
+
82
+ file_path = output_dir / filename
83
+ if is_audio:
84
+ with open(file_path, "wb") as f:
85
+ for chunk in resp.iter_content(chunk_size=8192):
86
+ f.write(chunk)
87
+ result.audio_path = str(file_path)
88
+ else:
89
+ file_path.write_text(resp.text, encoding="utf-8")
90
+
91
+ result.downloaded.append(filename)
92
+
93
+ return result
94
+
95
+
96
+ def register(app: typer.Typer) -> None:
97
+ """Register download and export commands on the converged-recordings app."""
98
+
99
+ @app.command("download")
100
+ def download(
101
+ recording_id: str = typer.Argument(help="Recording ID to download"),
102
+ include_audio: bool = typer.Option(False, "--include-audio", help="Download the MP3 audio file"),
103
+ output_dir: str = typer.Option("./recordings", "--output-dir", "-d", help="Target directory"),
104
+ debug: bool = typer.Option(False, "--debug"),
105
+ ):
106
+ """Download a single recording's artifacts to a local directory."""
107
+ api = get_api(debug=debug)
108
+ out_path = Path(output_dir) / recording_id
109
+
110
+ console.print(f"Downloading recording {recording_id}...")
111
+
112
+ try:
113
+ result = download_recording_artifacts(api, recording_id, out_path, include_audio)
114
+ except WebexError as e:
115
+ handle_rest_error(e)
116
+ return # unreachable — handle_rest_error always raises
117
+ except Exception as e:
118
+ console.print(f"[red]Error: {e}[/red]")
119
+ raise typer.Exit(1)
120
+
121
+ # Summary
122
+ parts = []
123
+ if result.downloaded:
124
+ parts.append(f"downloaded: {', '.join(result.downloaded)}")
125
+ if result.unavailable:
126
+ parts.append(f"not available: {', '.join(result.unavailable)}")
127
+ if result.failed:
128
+ parts.append(f"failed: {', '.join(result.failed)}")
129
+
130
+ console.print(f" Output: {out_path}")
131
+ console.print(f" {'; '.join(parts)}")
132
+
133
+ if not result.downloaded:
134
+ raise typer.Exit(1)
135
+
136
+ @app.command("export")
137
+ def export(
138
+ from_date: str = typer.Option(..., "--from", help="Start date, ISO 8601 (required)"),
139
+ to_date: str = typer.Option(..., "--to", help="End date, ISO 8601 (required)"),
140
+ owner_email: str = typer.Option(None, "--owner-email", help="Filter by owner email"),
141
+ owner_id: str = typer.Option(None, "--owner-id", help="Filter by owner ID"),
142
+ owner_type: str = typer.Option(None, "--owner-type", help="Filter: user|place|virtualLine|callQueue"),
143
+ location_id: str = typer.Option(None, "--location-id", help="Filter by location ID"),
144
+ service_type: str = typer.Option(None, "--service-type", help="Filter: calling|customerAssist"),
145
+ status: str = typer.Option(None, "--status", help="Filter: available|deleted"),
146
+ topic: str = typer.Option(None, "--topic", help="Filter by topic keyword"),
147
+ include_audio: bool = typer.Option(False, "--include-audio", help="Download MP3 audio files"),
148
+ fmt: str = typer.Option("jsonl", "--format", help="Output format: jsonl|json-per-file"),
149
+ output_dir: str = typer.Option("./recording-export", "--output-dir", "-d", help="Target directory"),
150
+ limit: int = typer.Option(0, "--limit", help="Max recordings to export (0=all)"),
151
+ debug: bool = typer.Option(False, "--debug"),
152
+ ):
153
+ """Bulk export recordings with text/AI artifacts for BI consumption."""
154
+ api = get_api(debug=debug)
155
+ out_path = Path(output_dir)
156
+ out_path.mkdir(parents=True, exist_ok=True)
157
+
158
+ # Build list query params
159
+ params: dict = {"from": from_date, "to": to_date}
160
+ if owner_email:
161
+ params["ownerEmail"] = owner_email
162
+ if owner_id:
163
+ params["ownerId"] = owner_id
164
+ if owner_type:
165
+ params["ownerType"] = owner_type
166
+ if location_id:
167
+ params["locationId"] = location_id
168
+ if service_type:
169
+ params["serviceType"] = service_type
170
+ if status:
171
+ params["status"] = status
172
+ if topic:
173
+ params["topic"] = topic
174
+
175
+ # Paginate the admin listing endpoint
176
+ console.print("Listing recordings...")
177
+ list_url = f"{BASE}/admin/convergedRecordings"
178
+ try:
179
+ recordings = list(api.session.follow_pagination(url=list_url, params=params, item_key="items"))
180
+ if limit > 0:
181
+ recordings = recordings[:limit]
182
+ except WebexError as e:
183
+ handle_rest_error(e)
184
+ return
185
+
186
+ if not recordings:
187
+ console.print("[yellow]No recordings found for the given filters.[/yellow]")
188
+ return
189
+
190
+ total = len(recordings)
191
+ console.print(f"Found {total} recording(s). Exporting...")
192
+
193
+ # Track stats
194
+ exported = 0
195
+ failed_ids: list[str] = []
196
+ artifact_counts: dict[str, int] = {"downloaded": 0, "unavailable": 0, "failed": 0}
197
+ audio_count = 0
198
+
199
+ # JSONL mode: open the output file
200
+ jsonl_file = None
201
+ if fmt == "jsonl":
202
+ jsonl_file = open(out_path / "recordings.jsonl", "w", encoding="utf-8")
203
+ if include_audio:
204
+ (out_path / "audio").mkdir(exist_ok=True)
205
+
206
+ try:
207
+ for i, rec in enumerate(recordings, 1):
208
+ rec_id = rec.get("id", "unknown")
209
+ console.print(f"Exporting recording {i}/{total} ({rec_id[:16]}...)...")
210
+
211
+ try:
212
+ if fmt == "json-per-file":
213
+ rec_dir = out_path / rec_id
214
+ dl_result = download_recording_artifacts(api, rec_id, rec_dir, include_audio)
215
+ else:
216
+ # JSONL mode: fetch detail, download artifacts inline
217
+ detail_url = f"{BASE}/convergedRecordings/{rec_id}"
218
+ detail = api.session.rest_get(detail_url)
219
+ links = detail.get("temporaryDirectDownloadLinks") or {}
220
+
221
+ # Build the JSONL record — all metadata fields except download links
222
+ record: dict = {
223
+ k: v for k, v in detail.items()
224
+ if k != "temporaryDirectDownloadLinks"
225
+ }
226
+
227
+ dl_result = DownloadResult(recording_id=rec_id, metadata=detail)
228
+
229
+ # Download text artifacts inline
230
+ for link_field, (filename, content_key, is_audio) in ARTIFACT_MAP.items():
231
+ if is_audio:
232
+ continue
233
+ download_url = links.get(link_field)
234
+ if not download_url:
235
+ record[content_key] = None
236
+ dl_result.unavailable.append(filename)
237
+ continue
238
+ try:
239
+ resp = requests.get(download_url, timeout=300)
240
+ resp.raise_for_status()
241
+ record[content_key] = resp.text
242
+ dl_result.downloaded.append(filename)
243
+ except Exception as e:
244
+ logger.warning("Failed to download %s for %s: %s", filename, rec_id, e)
245
+ record[content_key] = None
246
+ dl_result.failed.append(filename)
247
+
248
+ # Audio (opt-in)
249
+ if include_audio:
250
+ audio_url = links.get("audioDownloadLink")
251
+ if not audio_url:
252
+ record["audioFile"] = None
253
+ dl_result.unavailable.append("audio.mp3")
254
+ else:
255
+ try:
256
+ resp = requests.get(audio_url, timeout=300, stream=True)
257
+ resp.raise_for_status()
258
+ audio_path = out_path / "audio" / f"{rec_id}.mp3"
259
+ with open(audio_path, "wb") as af:
260
+ for chunk in resp.iter_content(chunk_size=8192):
261
+ af.write(chunk)
262
+ record["audioFile"] = f"audio/{rec_id}.mp3"
263
+ dl_result.audio_path = str(audio_path)
264
+ dl_result.downloaded.append("audio.mp3")
265
+ except Exception as e:
266
+ logger.warning("Failed to download audio for %s: %s", rec_id, e)
267
+ record["audioFile"] = None
268
+ dl_result.failed.append("audio.mp3")
269
+ else:
270
+ record["audioFile"] = None
271
+
272
+ jsonl_file.write(json.dumps(record, default=str) + "\n")
273
+ jsonl_file.flush()
274
+
275
+ exported += 1
276
+ artifact_counts["downloaded"] += len(dl_result.downloaded)
277
+ artifact_counts["unavailable"] += len(dl_result.unavailable)
278
+ artifact_counts["failed"] += len(dl_result.failed)
279
+ if dl_result.audio_path:
280
+ audio_count += 1
281
+
282
+ except WebexError as e:
283
+ logger.warning("Failed to export recording %s: %s", rec_id, e)
284
+ failed_ids.append(rec_id)
285
+ except Exception as e:
286
+ logger.warning("Failed to export recording %s: %s", rec_id, e)
287
+ failed_ids.append(rec_id)
288
+
289
+ finally:
290
+ if jsonl_file:
291
+ jsonl_file.close()
292
+
293
+ # Final summary
294
+ console.print("")
295
+ console.print("[bold]Export Summary[/bold]")
296
+ console.print(f" Total recordings: {total}")
297
+ console.print(f" Exported: {exported}")
298
+ console.print(f" Failed: {len(failed_ids)}")
299
+ console.print(f" Artifacts downloaded: {artifact_counts['downloaded']}")
300
+ console.print(f" Artifacts unavailable: {artifact_counts['unavailable']}")
301
+ console.print(f" Artifacts failed: {artifact_counts['failed']}")
302
+ if include_audio:
303
+ console.print(f" Audio files: {audio_count}")
304
+ console.print(f" Output directory: {out_path}")
305
+
306
+ if failed_ids:
307
+ console.print(f"\n[yellow]Failed recording IDs:[/yellow]")
308
+ for fid in failed_ids:
309
+ console.print(f" - {fid}")
310
+ raise typer.Exit(1)