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,279 @@
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
+ from wxcli.config import get_org_id
7
+
8
+
9
+ app = typer.Typer(help="Manage Webex Calling workspaces.")
10
+
11
+
12
+ @app.command("list")
13
+ def cmd_list(
14
+ location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Values must originat"),
15
+ workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Location associated with the workspace. Both values from the"),
16
+ floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
17
+ display_name: str = typer.Option(None, "--display-name", help="List workspaces by display name."),
18
+ capacity: str = typer.Option(None, "--capacity", help="List workspaces with the given capacity. Must be -1 or highe"),
19
+ type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
20
+ calling: str = typer.Option(None, "--calling", help="Choices: freeCalling, hybridCalling, webexCalling, webexEdgeForDevices, thirdPartySipCalling, none"),
21
+ supported_devices: str = typer.Option(None, "--supported-devices", help="Choices: collaborationDevices, phones"),
22
+ calendar: str = typer.Option(None, "--calendar", help="Choices: none, google, microsoft"),
23
+ device_hosted_meetings_enabled: str = typer.Option(None, "--device-hosted-meetings-enabled", help="List workspaces enabled for device hosted meetings."),
24
+ device_platform: str = typer.Option(None, "--device-platform", help="Choices: cisco, microsoftTeamsRoom"),
25
+ health_level: str = typer.Option(None, "--health-level", help="Choices: error, warning, info, ok"),
26
+ include_devices: str = typer.Option(None, "--include-devices", help="Flag identifying whether to include the devices associated w"),
27
+ include_capabilities: str = typer.Option(None, "--include-capabilities", help="Flag identifying whether to include the workspace capabiliti"),
28
+ planned_maintenance: str = typer.Option(None, "--planned-maintenance", help="Choices: off, on, upcoming"),
29
+ custom_attribute: str = typer.Option(None, "--custom-attribute", help="List workspaces with given custom attribute key."),
30
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
31
+ limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
32
+ offset: int = typer.Option(0, "--offset", help="Start offset"),
33
+ debug: bool = typer.Option(False, "--debug"),
34
+ ):
35
+ """List Workspaces."""
36
+ api = get_api(debug=debug)
37
+ url = f"https://webexapis.com/v1/workspaces"
38
+ params = {}
39
+ if location_id is not None:
40
+ params["locationId"] = location_id
41
+ if workspace_location_id is not None:
42
+ params["workspaceLocationId"] = workspace_location_id
43
+ if floor_id is not None:
44
+ params["floorId"] = floor_id
45
+ if display_name is not None:
46
+ params["displayName"] = display_name
47
+ if capacity is not None:
48
+ params["capacity"] = capacity
49
+ if type_param is not None:
50
+ params["type"] = type_param
51
+ if calling is not None:
52
+ params["calling"] = calling
53
+ if supported_devices is not None:
54
+ params["supportedDevices"] = supported_devices
55
+ if calendar is not None:
56
+ params["calendar"] = calendar
57
+ if device_hosted_meetings_enabled is not None:
58
+ params["deviceHostedMeetingsEnabled"] = device_hosted_meetings_enabled
59
+ if device_platform is not None:
60
+ params["devicePlatform"] = device_platform
61
+ if health_level is not None:
62
+ params["healthLevel"] = health_level
63
+ if include_devices is not None:
64
+ params["includeDevices"] = include_devices
65
+ if include_capabilities is not None:
66
+ params["includeCapabilities"] = include_capabilities
67
+ if planned_maintenance is not None:
68
+ params["plannedMaintenance"] = planned_maintenance
69
+ if custom_attribute is not None:
70
+ params["customAttribute"] = custom_attribute
71
+ if limit > 0:
72
+ params["max"] = limit
73
+ if offset > 0:
74
+ params["start"] = offset
75
+ org_id = get_org_id()
76
+ if org_id is not None:
77
+ params["orgId"] = org_id
78
+ result = None
79
+ try:
80
+ result = api.session.rest_get(url, params=params)
81
+ except WebexError as e:
82
+ handle_rest_error(e)
83
+ result = result or []
84
+ items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
85
+ if output == "json":
86
+ print_json(items)
87
+ else:
88
+ print_table(items, columns=[('ID', 'id'), ('Display Name', 'displayName'), ('Type', 'type'), ('Calling', 'calling.type')], limit=limit)
89
+
90
+
91
+
92
+ @app.command("create")
93
+ def create(
94
+ display_name: str = typer.Option(None, "--display-name", help="(required) A friendly name for the workspace."),
95
+ org_id: str = typer.Option(None, "--org-id", help="`OrgId` associated with the workspace. Only admin users of a"),
96
+ location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Must be provided whe"),
97
+ workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Legacy workspace location ID associated with the workspace."),
98
+ floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
99
+ capacity: str = typer.Option(None, "--capacity", help="How many people the workspace is suitable for. If set, must"),
100
+ type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
101
+ sip_address: str = typer.Option(None, "--sip-address", help="The `sipAddress` field can only be provided when calling typ"),
102
+ notes: str = typer.Option(None, "--notes", help="Notes associated to the workspace."),
103
+ hotdesking_status: str = typer.Option(None, "--hotdesking-status", help="Choices: on, off"),
104
+ supported_devices: str = typer.Option(None, "--supported-devices", help="Choices: collaborationDevices, phones"),
105
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
106
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
107
+ debug: bool = typer.Option(False, "--debug"),
108
+ ):
109
+ """Create a Workspace\n\nExample --json-body:\n '{"displayName":"...","orgId":"...","locationId":"...","workspaceLocationId":"...","floorId":"...","capacity":0,"type":"notSet","sipAddress":"..."}'."""
110
+ api = get_api(debug=debug)
111
+ url = f"https://webexapis.com/v1/workspaces"
112
+ if json_body:
113
+ body = json.loads(json_body)
114
+ else:
115
+ body = {}
116
+ if display_name is not None:
117
+ body["displayName"] = display_name
118
+ if org_id is not None:
119
+ body["orgId"] = org_id
120
+ if location_id is not None:
121
+ body["locationId"] = location_id
122
+ if workspace_location_id is not None:
123
+ body["workspaceLocationId"] = workspace_location_id
124
+ if floor_id is not None:
125
+ body["floorId"] = floor_id
126
+ if capacity is not None:
127
+ body["capacity"] = capacity
128
+ if type_param is not None:
129
+ body["type"] = type_param
130
+ if sip_address is not None:
131
+ body["sipAddress"] = sip_address
132
+ if notes is not None:
133
+ body["notes"] = notes
134
+ if hotdesking_status is not None:
135
+ body["hotdeskingStatus"] = hotdesking_status
136
+ if supported_devices is not None:
137
+ body["supportedDevices"] = supported_devices
138
+ _missing = [f for f in ['displayName'] if f not in body or body[f] is None]
139
+ if _missing:
140
+ typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
141
+ raise typer.Exit(1)
142
+ try:
143
+ result = api.session.rest_post(url, json=body)
144
+ except WebexError as e:
145
+ handle_rest_error(e)
146
+ if output == "json":
147
+ print_json(result)
148
+ elif isinstance(result, dict) and "id" in result:
149
+ typer.echo(f"Created: {result['id']}")
150
+ elif not result or result == {}:
151
+ typer.echo("Created.")
152
+ else:
153
+ print_json(result)
154
+
155
+
156
+
157
+ @app.command("show")
158
+ def show(
159
+ workspace_id: str = typer.Argument(help="workspaceId"),
160
+ include_devices: str = typer.Option(None, "--include-devices", help="Flag identifying whether to include the devices associated w"),
161
+ include_capabilities: str = typer.Option(None, "--include-capabilities", help="Flag identifying whether to include the workspace capabiliti"),
162
+ output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
163
+ debug: bool = typer.Option(False, "--debug"),
164
+ ):
165
+ """Get Workspace Details."""
166
+ api = get_api(debug=debug)
167
+ url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
168
+ params = {}
169
+ if include_devices is not None:
170
+ params["includeDevices"] = include_devices
171
+ if include_capabilities is not None:
172
+ params["includeCapabilities"] = include_capabilities
173
+ try:
174
+ result = api.session.rest_get(url, params=params)
175
+ except WebexError as e:
176
+ handle_rest_error(e)
177
+ if output == "json":
178
+ print_json(result)
179
+ else:
180
+ if isinstance(result, dict):
181
+ print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
182
+ elif isinstance(result, list):
183
+ print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
184
+ else:
185
+ print_json(result)
186
+
187
+
188
+
189
+ @app.command("update")
190
+ def update(
191
+ workspace_id: str = typer.Argument(help="workspaceId"),
192
+ display_name: str = typer.Option(None, "--display-name", help="A friendly name for the workspace."),
193
+ location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Must be provided whe"),
194
+ workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Legacy workspace location ID associated with the workspace."),
195
+ floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
196
+ capacity: str = typer.Option(None, "--capacity", help="How many people the workspace is suitable for. If set, must"),
197
+ type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
198
+ sip_address: str = typer.Option(None, "--sip-address", help="The `sipAddress` field can only be provided when calling typ"),
199
+ notes: str = typer.Option(None, "--notes", help="Notes associated to the workspace."),
200
+ hotdesking_status: str = typer.Option(None, "--hotdesking-status", help="Choices: on, off"),
201
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
202
+ debug: bool = typer.Option(False, "--debug"),
203
+ ):
204
+ """Update a Workspace\n\nExample --json-body:\n '{"displayName":"...","locationId":"...","workspaceLocationId":"...","floorId":"...","capacity":0,"type":"notSet","calendar":{"type":"...","emailAddress":"...","resourceGroupId":"..."},"sipAddress":"..."}'."""
205
+ api = get_api(debug=debug)
206
+ url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
207
+ if json_body:
208
+ body = json.loads(json_body)
209
+ else:
210
+ body = {}
211
+ if display_name is not None:
212
+ body["displayName"] = display_name
213
+ if location_id is not None:
214
+ body["locationId"] = location_id
215
+ if workspace_location_id is not None:
216
+ body["workspaceLocationId"] = workspace_location_id
217
+ if floor_id is not None:
218
+ body["floorId"] = floor_id
219
+ if capacity is not None:
220
+ body["capacity"] = capacity
221
+ if type_param is not None:
222
+ body["type"] = type_param
223
+ if sip_address is not None:
224
+ body["sipAddress"] = sip_address
225
+ if notes is not None:
226
+ body["notes"] = notes
227
+ if hotdesking_status is not None:
228
+ body["hotdeskingStatus"] = hotdesking_status
229
+ try:
230
+ result = api.session.rest_put(url, json=body)
231
+ except WebexError as e:
232
+ handle_rest_error(e)
233
+ typer.echo(f"Updated.")
234
+
235
+
236
+
237
+ @app.command("delete")
238
+ def delete(
239
+ workspace_id: str = typer.Argument(help="workspaceId"),
240
+ force: bool = typer.Option(False, "--force", help="Skip confirmation"),
241
+ debug: bool = typer.Option(False, "--debug"),
242
+ ):
243
+ """Delete a Workspace."""
244
+ if not force:
245
+ typer.confirm(f"Delete {workspace_id}?", abort=True)
246
+ api = get_api(debug=debug)
247
+ url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
248
+ try:
249
+ api.session.rest_delete(url)
250
+ except WebexError as e:
251
+ handle_rest_error(e)
252
+ typer.echo(f"Deleted: {workspace_id}")
253
+
254
+
255
+
256
+ @app.command("show-capabilities")
257
+ def show_capabilities(
258
+ workspace_id: str = typer.Argument(help="workspaceId"),
259
+ output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
260
+ debug: bool = typer.Option(False, "--debug"),
261
+ ):
262
+ """Get Workspace Capabilities."""
263
+ api = get_api(debug=debug)
264
+ url = f"https://webexapis.com/v1/workspaces/{workspace_id}/capabilities"
265
+ try:
266
+ result = api.session.rest_get(url)
267
+ except WebexError as e:
268
+ handle_rest_error(e)
269
+ if output == "json":
270
+ print_json(result)
271
+ else:
272
+ if isinstance(result, dict):
273
+ print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
274
+ elif isinstance(result, list):
275
+ print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
276
+ else:
277
+ print_json(result)
278
+
279
+
wxcli/commands/xapi.py ADDED
@@ -0,0 +1,114 @@
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 xapi.")
9
+
10
+
11
+ @app.command("show")
12
+ def show(
13
+ device_id: str = typer.Option(..., "--device-id", help="The unique identifier for the Webex RoomOS Device."),
14
+ name: str = typer.Option(..., "--name", help="A list of status expressions used to query the Webex RoomOS"),
15
+ output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
16
+ debug: bool = typer.Option(False, "--debug"),
17
+ ):
18
+ """Query Status."""
19
+ api = get_api(debug=debug)
20
+ url = f"https://webexapis.com/v1/xapi/status"
21
+ params = {}
22
+ if device_id is not None:
23
+ params["deviceId"] = device_id
24
+ if name is not None:
25
+ params["name"] = name
26
+ try:
27
+ result = api.session.rest_get(url, params=params)
28
+ except WebexError as e:
29
+ handle_rest_error(e)
30
+ if output == "json":
31
+ print_json(result)
32
+ else:
33
+ if isinstance(result, dict):
34
+ print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
35
+ elif isinstance(result, list):
36
+ print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
37
+ else:
38
+ print_json(result)
39
+
40
+
41
+
42
+ @app.command("list")
43
+ def cmd_list(
44
+ device_id: str = typer.Option(..., "--device-id", help="A list of device IDs to query schemas from. A request can co"),
45
+ status: str = typer.Option(None, "--status", help="A list of status key expressions to query schemas for. Suppo"),
46
+ command: str = typer.Option(None, "--command", help="A list of command key expressions to query schemas for. Supp"),
47
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
48
+ limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
49
+ offset: int = typer.Option(0, "--offset", help="Start offset"),
50
+ debug: bool = typer.Option(False, "--debug"),
51
+ ):
52
+ """Query Schema."""
53
+ api = get_api(debug=debug)
54
+ url = f"https://webexapis.com/v1/xapi/schema"
55
+ params = {}
56
+ if device_id is not None:
57
+ params["deviceId"] = device_id
58
+ if status is not None:
59
+ params["status"] = status
60
+ if command is not None:
61
+ params["command"] = command
62
+ if limit > 0:
63
+ params["max"] = limit
64
+ if offset > 0:
65
+ params["start"] = offset
66
+ result = None
67
+ try:
68
+ result = api.session.rest_get(url, params=params)
69
+ except WebexError as e:
70
+ handle_rest_error(e)
71
+ result = result or []
72
+ items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
73
+ if output == "json":
74
+ print_json(items)
75
+ else:
76
+ print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
77
+
78
+
79
+
80
+ @app.command("create")
81
+ def create(
82
+ command_name: str = typer.Argument(help="commandName"),
83
+ device_id: str = typer.Option(None, "--device-id", help="(required) The unique identifier for the Webex RoomOS Device."),
84
+ json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
85
+ output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
86
+ debug: bool = typer.Option(False, "--debug"),
87
+ ):
88
+ """Execute Command\n\nExample --json-body:\n '{"deviceId":"...","arguments":{"Level":0},"body":{"Booking":{"Id":"...","Title":"...","Protocol":"...","Time":"...","Organizer":"...","Number":"..."}}}'."""
89
+ api = get_api(debug=debug)
90
+ url = f"https://webexapis.com/v1/xapi/command/{command_name}"
91
+ if json_body:
92
+ body = json.loads(json_body)
93
+ else:
94
+ body = {}
95
+ if device_id is not None:
96
+ body["deviceId"] = device_id
97
+ _missing = [f for f in ['deviceId'] if f not in body or body[f] is None]
98
+ if _missing:
99
+ typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
100
+ raise typer.Exit(1)
101
+ try:
102
+ result = api.session.rest_post(url, json=body)
103
+ except WebexError as e:
104
+ handle_rest_error(e)
105
+ if output == "json":
106
+ print_json(result)
107
+ elif isinstance(result, dict) and "id" in result:
108
+ typer.echo(f"Created: {result['id']}")
109
+ elif not result or result == {}:
110
+ typer.echo("Created.")
111
+ else:
112
+ print_json(result)
113
+
114
+
wxcli/config.py ADDED
@@ -0,0 +1,139 @@
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+
5
+ DEFAULT_CONFIG_PATH = Path.home() / ".wxcli" / "config.json"
6
+
7
+ def load_config(path: Path = DEFAULT_CONFIG_PATH) -> dict:
8
+ if not path.exists():
9
+ return {"profiles": {}}
10
+ with open(path, encoding="utf-8") as f:
11
+ return json.load(f)
12
+
13
+ def save_config(data: dict, path: Path = DEFAULT_CONFIG_PATH) -> None:
14
+ path.parent.mkdir(parents=True, exist_ok=True)
15
+ with open(path, "w", encoding="utf-8") as f:
16
+ json.dump(data, f, indent=2)
17
+
18
+ def get_token(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
19
+ config = load_config(path)
20
+ profile = config.get("profiles", {}).get("default", {})
21
+ return profile.get("token")
22
+
23
+ def get_expires_at(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
24
+ config = load_config(path)
25
+ profile = config.get("profiles", {}).get("default", {})
26
+ return profile.get("expires_at")
27
+
28
+ def get_org_id(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
29
+ config = load_config(path)
30
+ profile = config.get("profiles", {}).get("default", {})
31
+ return profile.get("org_id")
32
+
33
+ def get_org_name(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
34
+ config = load_config(path)
35
+ profile = config.get("profiles", {}).get("default", {})
36
+ return profile.get("org_name")
37
+
38
+ def decode_webex_id(encoded_id: str) -> str:
39
+ """Decode a base64-encoded Webex Spark ID to its UUID suffix.
40
+
41
+ Webex Calling APIs return orgId as a base64 URN like:
42
+ Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9iODQxMDE0Ny02MTA0LTQyZTgt...
43
+ which decodes to: ciscospark://us/ORGANIZATION/b8410147-6104-42e8-9b93-...
44
+
45
+ The Contact Center API requires the bare UUID (after the last '/'),
46
+ not the full base64 string.
47
+ """
48
+ try:
49
+ padding = 4 - len(encoded_id) % 4
50
+ if padding != 4:
51
+ encoded_id += "=" * padding
52
+ decoded = base64.b64decode(encoded_id).decode("utf-8")
53
+ return decoded.rsplit("/", 1)[-1]
54
+ except Exception:
55
+ return encoded_id
56
+
57
+
58
+ def resolve_org_id(api_session) -> str:
59
+ """Return the org ID from config, or fetch it from /v1/people/me."""
60
+ saved = get_org_id()
61
+ if saved:
62
+ return saved
63
+ return api_session.rest_get("https://webexapis.com/v1/people/me").get("orgId", "")
64
+
65
+
66
+ def get_cc_org_id(api_session) -> str:
67
+ """Return the decoded UUID org ID suitable for CC API path parameters.
68
+
69
+ Uses the saved org_id from config if available (already decoded), otherwise
70
+ fetches from people/me and decodes the base64 Spark ID.
71
+ """
72
+ saved = get_org_id()
73
+ if saved:
74
+ return decode_webex_id(saved)
75
+ raw = api_session.rest_get("https://webexapis.com/v1/people/me").get("orgId", "")
76
+ return decode_webex_id(raw)
77
+
78
+
79
+ CC_REGIONS = {
80
+ "us1": "https://api.wxcc-us1.cisco.com",
81
+ "eu1": "https://api.wxcc-eu1.cisco.com",
82
+ "eu2": "https://api.wxcc-eu2.cisco.com",
83
+ "anz1": "https://api.wxcc-anz1.cisco.com",
84
+ "ca1": "https://api.wxcc-ca1.cisco.com",
85
+ "jp1": "https://api.wxcc-jp1.cisco.com",
86
+ "sg1": "https://api.wxcc-sg1.cisco.com",
87
+ }
88
+
89
+ def get_cc_base_url(path: Path = DEFAULT_CONFIG_PATH) -> str:
90
+ config = load_config(path)
91
+ profile = config.get("profiles", {}).get("default", {})
92
+ region = profile.get("cc_region", "us1")
93
+ return CC_REGIONS.get(region, CC_REGIONS["us1"])
94
+
95
+ def save_cc_region(region: str, path: Path = DEFAULT_CONFIG_PATH) -> None:
96
+ config = load_config(path)
97
+ profile = config.setdefault("profiles", {}).setdefault("default", {})
98
+ profile["cc_region"] = region
99
+ save_config(config, path)
100
+
101
+ FS_DATACENTERS = {
102
+ "intgus1": "https://flow-store.intgus1.ciscoccservice.com/flow-store",
103
+ "us1": "https://flow-store.us1.ciscoccservice.com/flow-store",
104
+ }
105
+
106
+ def get_fs_base_url(path: Path = DEFAULT_CONFIG_PATH) -> str:
107
+ config = load_config(path)
108
+ profile = config.get("profiles", {}).get("default", {})
109
+ dc = profile.get("fs_datacenter", "intgus1")
110
+ return FS_DATACENTERS.get(dc, FS_DATACENTERS["intgus1"])
111
+
112
+ def get_fs_project_id(path: Path = DEFAULT_CONFIG_PATH) -> str:
113
+ config = load_config(path)
114
+ profile = config.get("profiles", {}).get("default", {})
115
+ pid = profile.get("fs_project_id")
116
+ if pid:
117
+ return pid
118
+ raise SystemExit(
119
+ "Flow Store project ID not configured. Run:\n"
120
+ " wxcli fs-projects list # to find your project ID\n"
121
+ " Then set it in ~/.wxcli/config.json under profiles.default.fs_project_id"
122
+ )
123
+
124
+ def save_fs_datacenter(datacenter: str, path: Path = DEFAULT_CONFIG_PATH) -> None:
125
+ config = load_config(path)
126
+ profile = config.setdefault("profiles", {}).setdefault("default", {})
127
+ profile["fs_datacenter"] = datacenter
128
+ save_config(config, path)
129
+
130
+ def save_org(org_id: str | None, org_name: str | None, path: Path = DEFAULT_CONFIG_PATH) -> None:
131
+ config = load_config(path)
132
+ profile = config.setdefault("profiles", {}).setdefault("default", {})
133
+ if org_id:
134
+ profile["org_id"] = org_id
135
+ profile["org_name"] = org_name
136
+ else:
137
+ profile.pop("org_id", None)
138
+ profile.pop("org_name", None)
139
+ save_config(config, path)
wxcli/errors.py ADDED
@@ -0,0 +1,77 @@
1
+ """Centralized error handler for wxcli commands."""
2
+ import json
3
+ import typer
4
+
5
+
6
+ class WebexError(Exception):
7
+ """Webex API error. Message-first: WebexError(response.text) remains valid.
8
+
9
+ status_code and body (parsed JSON error, when parseable) are optional
10
+ enrichments so handlers can key off status instead of substring matching.
11
+ """
12
+
13
+ def __init__(self, message: str, status_code: int | None = None, body: dict | None = None):
14
+ super().__init__(message)
15
+ self.status_code = status_code
16
+ self.body = body
17
+
18
+
19
+ _ERROR_TIPS = {
20
+ 4003: "This endpoint requires a user-level OAuth token, not an admin or service app token.",
21
+ 4008: "This endpoint requires the target user to have a Webex Calling license.",
22
+ 9601: "This endpoint requires a user-level OAuth token, not an admin or service app token.",
23
+ 25008: "Use --json-body for full control over the request body.",
24
+ 25409: "This workspace setting requires a Professional license. Use -o json with the /features/ path commands for Basic workspaces.",
25
+ 28018: "CX Essentials is not enabled for this queue. Use --has-cx-essentials true when creating/querying CX queues.",
26
+ }
27
+
28
+ _MESSAGE_TIPS = {
29
+ "Target user not authorized": "This endpoint requires a user-level OAuth token, not an admin or service app token.",
30
+ }
31
+
32
+
33
+ def _extract_error_code(err: str) -> int | None:
34
+ """Extract error code from a Webex API JSON error response."""
35
+ try:
36
+ body = json.loads(err)
37
+ for entry in body.get("errors", []):
38
+ code = entry.get("errorCode") or entry.get("code")
39
+ if code is not None:
40
+ return int(code)
41
+ except (json.JSONDecodeError, ValueError, AttributeError):
42
+ pass
43
+ return None
44
+
45
+
46
+ def _truncate_html(err: str) -> str:
47
+ """Extract a readable message from HTML error responses."""
48
+ if not err.lstrip().startswith("<"):
49
+ return err
50
+ import re
51
+ title = re.search(r"<title>(.*?)</title>", err, re.IGNORECASE)
52
+ h1 = re.search(r"<h1>(.*?)</h1>", err, re.IGNORECASE)
53
+ msg = (h1 and h1.group(1)) or (title and title.group(1)) or "Non-JSON error response"
54
+ return msg.strip()
55
+
56
+
57
+ def handle_rest_error(e: WebexError) -> None:
58
+ """Centralized error handler with actionable tips."""
59
+ err = str(e)
60
+ code = _extract_error_code(err)
61
+
62
+ if code == 4003 and "Target user not authorized" not in err:
63
+ code = None
64
+
65
+ if code and code in _ERROR_TIPS:
66
+ typer.echo(f"Error: {_truncate_html(err)}", err=True)
67
+ typer.echo(f"Tip: {_ERROR_TIPS[code]}", err=True)
68
+ elif any(msg in err for msg in _MESSAGE_TIPS):
69
+ tip = next(v for k, v in _MESSAGE_TIPS.items() if k in err)
70
+ typer.echo(f"Error: {_truncate_html(err)}", err=True)
71
+ typer.echo(f"Tip: {tip}", err=True)
72
+ elif "wxcc" in err and "403" in err:
73
+ typer.echo(f"Error: {_truncate_html(err)}", err=True)
74
+ typer.echo("Tip: Contact Center APIs require CC-scoped OAuth (cjp:config_read / cjp:config_write). Standard admin tokens won't work.", err=True)
75
+ else:
76
+ typer.echo(f"Error: {_truncate_html(err)}", err=True)
77
+ raise typer.Exit(1)