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,1334 @@
1
+ # Location Call Settings — Announcements, Playlists, Schedules & Access Codes
2
+
3
+ ## Sources
4
+
5
+ - wxc_sdk v1.30.0
6
+ - OpenAPI spec: specs/webex-cloud-calling.json
7
+ - developer.webex.com Location Call Settings APIs
8
+
9
+ Reference for managing audio media (announcements, playlists), time-based routing (schedules), and outgoing-permission bypass codes at the location and org level via the `wxc_sdk`.
10
+
11
+ > **Not supported for Webex for Government (FedRAMP)** — Announcements and Playlists APIs are explicitly excluded. See [authentication.md → FedRAMP](authentication.md#webex-for-government-fedramp) for all FedRAMP restrictions.
12
+
13
+ ---
14
+
15
+ ## 1. Announcements Repository
16
+
17
+ The announcement repository stores binary audio files (WAV) used by Auto Attendants, Call Queues, and Music On Hold. Files can be uploaded at the **organization level** or scoped to a **specific location**.
18
+
19
+ **SDK access path:** `api.telephony.announcements_repo`
20
+
21
+ **API class:** `AnnouncementsRepositoryApi` (base: `telephony/config`)
22
+
23
+ ### 1.1 Data Models
24
+
25
+ #### `RepoAnnouncement` (extends `IdAndName`)
26
+
27
+ | Field | Type | Notes |
28
+ |-------|------|-------|
29
+ | `id` | `str` | Unique identifier |
30
+ | `name` | `str` | Display name |
31
+ | `file_name` | `str` | Uploaded binary file name |
32
+ | `file_size` | `int` | Size in kilobytes |
33
+ | `media_file_type` | `MediaFileType` | Audio/video media type |
34
+ | `last_updated` | `datetime` | UTC timestamp |
35
+ | `level` | `AnnouncementLevel` | Org-level vs. location-level |
36
+ | `location` | `IdAndName` | Location details (if location-scoped) |
37
+ | `feature_reference_count` | `int` | Number of features referencing this file (**details only**, not returned by list) |
38
+ | `feature_references` | `list[FeatureReference]` | Features using this announcement (**details only**) |
39
+ | `playlists` | `list[IdAndName]` | Playlists containing this announcement (**details only**) |
40
+
41
+ #### `FeatureReference`
42
+
43
+ Describes a call feature (Auto Attendant, Call Queue, Music On Hold) that references an announcement.
44
+
45
+ | Field | Type | Notes |
46
+ |-------|------|-------|
47
+ | `id` | `str` | Feature identifier |
48
+ | `name` | `str` | Feature name |
49
+ | `type` | `str` | Resource type of the feature |
50
+ | `location_id` | `str` | Location identifier |
51
+ | `location_name` | `str` | Location name |
52
+
53
+ #### `RepositoryUsage`
54
+
55
+ | Field | Type | Notes |
56
+ |-------|------|-------|
57
+ | `total_file_size_used_kb` | `int` | Total space used (KB) |
58
+ | `max_audio_file_size_allowed_kb` | `int` | Max single audio file size (KB) |
59
+ | `max_video_file_size_allowed_kb` | `int` | Max single video file size (KB) |
60
+ | `total_file_size_limit_mb` | `int` | Total repo capacity (MB) |
61
+
62
+ ### 1.2 Methods
63
+
64
+ #### `list(...)` — List announcement greetings
65
+
66
+ ```python
67
+ def list(
68
+ self,
69
+ location_id: str = None, # None = org-level; "all" or "locations" or specific ID
70
+ order: str = None, # Sort by "fileName" or "fileSize" (ascending default)
71
+ file_name: str = None, # Filter by file name
72
+ file_type: str = None, # Filter by file type
73
+ media_file_type: str = None, # Filter by media file type
74
+ name: str = None, # Filter by announcement label
75
+ org_id: str = None,
76
+ **params
77
+ ) -> Generator[RepoAnnouncement, None, None]
78
+ ```
79
+
80
+ - Returns a **paginated generator** of `RepoAnnouncement` objects.
81
+ - Without `location_id`, returns enterprise (org) level announcements.
82
+ - With `location_id` set to a specific location ID, returns that location's announcements.
83
+ - Special values for `location_id`: `"all"` (all levels), `"locations"` (all location-level).
84
+
85
+ **Required scope:** `spark-admin:telephony_config_read`
86
+
87
+ #### `upload_announcement(...)` — Upload a new audio file
88
+
89
+ ```python
90
+ def upload_announcement(
91
+ self,
92
+ name: str, # Display name (required)
93
+ file: Union[BufferedReader, str], # File path (str) or open binary reader
94
+ upload_as: str = None, # Filename for upload (required if file is a reader)
95
+ location_id: str = None, # None = org level
96
+ org_id: str = None,
97
+ ) -> str # Returns the new announcement ID
98
+ ```
99
+
100
+ - Accepts a **file path** (str) or a **BufferedReader** / `BytesIO` object.
101
+ - When passing a reader, `upload_as` is required and must be a `.wav` filename.
102
+ - Uses `multipart/form-data` with `audio/wav` content type internally.
103
+ - Without `location_id`, uploads to org-level repository.
104
+ - With `location_id`, uploads to that location's repository.
105
+
106
+ **Required scope:** `spark-admin:telephony_config_write`
107
+
108
+ **Usage examples:**
109
+
110
+ ```python
111
+ # Upload from file path
112
+ ann_id = api.telephony.announcements_repo.upload_announcement(
113
+ name='Welcome Greeting', file='/path/to/greeting.wav'
114
+ )
115
+
116
+ # Upload from open file handle
117
+ with open('greeting.wav', mode='rb') as f:
118
+ ann_id = api.telephony.announcements_repo.upload_announcement(
119
+ name='Welcome Greeting', file=f, upload_as='greeting.wav'
120
+ )
121
+
122
+ # Upload from bytes via BytesIO
123
+ import io
124
+ binary_file = io.BytesIO(wav_bytes)
125
+ ann_id = api.telephony.announcements_repo.upload_announcement(
126
+ name='Welcome Greeting', file=binary_file, upload_as='greeting.wav'
127
+ )
128
+
129
+ # Upload to a specific location
130
+ ann_id = api.telephony.announcements_repo.upload_announcement(
131
+ name='Location Greeting', file='greeting.wav', location_id='<location_id>'
132
+ )
133
+ ```
134
+
135
+ #### `details(...)` — Get single announcement details
136
+
137
+ ```python
138
+ def details(
139
+ self,
140
+ announcement_id: str,
141
+ location_id: str = None, # None = org level
142
+ org_id: str = None,
143
+ ) -> RepoAnnouncement
144
+ ```
145
+
146
+ - Returns `feature_reference_count`, `feature_references`, and `playlists` (which `list()` does not).
147
+
148
+ **Required scope:** `spark-admin:telephony_config_read`
149
+
150
+ #### `modify(...)` — Replace an existing announcement file
151
+
152
+ ```python
153
+ def modify(
154
+ self,
155
+ announcement_id: str,
156
+ name: str, # New display name
157
+ file: Union[BufferedReader, str], # Replacement file
158
+ upload_as: str = None,
159
+ location_id: str = None,
160
+ org_id: str = None,
161
+ ) -> str # Returns announcement ID
162
+ ```
163
+
164
+ - Same file-handling semantics as `upload_announcement`.
165
+ - Uses `multipart/form-data` PUT request internally.
166
+
167
+ **Required scope:** `spark-admin:telephony_config_write`
168
+
169
+ #### `delete(...)` — Delete an announcement
170
+
171
+ ```python
172
+ def delete(
173
+ self,
174
+ announcement_id: str,
175
+ location_id: str = None, # None = org level
176
+ org_id: str = None,
177
+ )
178
+ ```
179
+
180
+ **Required scope:** `spark-admin:telephony_config_write`
181
+
182
+ #### `usage(...)` — Get repository storage usage
183
+
184
+ ```python
185
+ def usage(
186
+ self,
187
+ location_id: str = None, # None = org level
188
+ org_id: str = None,
189
+ ) -> RepositoryUsage
190
+ ```
191
+
192
+ - Returns file size limits and current usage.
193
+
194
+ **Required scope:** `spark-admin:telephony_config_read`
195
+
196
+ ### 1.3 Raw HTTP — Announcements
197
+
198
+ ```python
199
+ BASE = "https://webexapis.com/v1"
200
+
201
+ # --- Org-level announcements ---
202
+
203
+ # GET — List org-level announcements (paginated)
204
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements",
205
+ params={"max": 1000})
206
+ # Response: {"announcements": [{"id": "...", "name": "...", "fileName": "...", "fileSize": 123, ...}]}
207
+
208
+ # GET — Get org-level announcement details
209
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements/{ann_id}")
210
+ # Response includes featureReferenceCount, featureReferences, playlists (not returned by list)
211
+
212
+ # PUT — Update org-level announcement (metadata only; file upload uses multipart)
213
+ body = {"name": "Updated Name"}
214
+ api.session.rest_put(f"{BASE}/telephony/config/announcements/{ann_id}", json=body)
215
+
216
+ # DELETE — Delete org-level announcement
217
+ api.session.rest_delete(f"{BASE}/telephony/config/announcements/{ann_id}")
218
+
219
+ # GET — Org-level repository usage
220
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements/usage")
221
+ # Response: {"totalFileSizeUsedKB": 1024, "maxAudioFileSizeAllowedKB": 8192,
222
+ # "maxVideoFileSizeAllowedKB": 16384, "totalFileSizeLimitMB": 500}
223
+
224
+ # --- Location-level announcements ---
225
+
226
+ # GET — List location announcements (use locationId query param on org endpoint)
227
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements",
228
+ params={"locationId": loc_id, "max": 1000})
229
+
230
+ # GET — Get location announcement details
231
+ result = api.session.rest_get(
232
+ f"{BASE}/telephony/config/locations/{loc_id}/announcements/{ann_id}")
233
+
234
+ # PUT — Update location announcement
235
+ api.session.rest_put(
236
+ f"{BASE}/telephony/config/locations/{loc_id}/announcements/{ann_id}", json=body)
237
+
238
+ # DELETE — Delete location announcement
239
+ api.session.rest_delete(
240
+ f"{BASE}/telephony/config/locations/{loc_id}/announcements/{ann_id}")
241
+
242
+ # GET — Location repository usage
243
+ result = api.session.rest_get(
244
+ f"{BASE}/telephony/config/locations/{loc_id}/announcements/usage")
245
+ ```
246
+
247
+ **Gotcha:** The list endpoint uses a query parameter `locationId` on the org-level URL (`/telephony/config/announcements`), but details/update/delete for location announcements use a path segment (`/locations/{locId}/announcements/{annId}`). These are different URL patterns for list vs. CRUD.
248
+ <!-- Updated by playbook session 2026-03-18 -->
249
+
250
+ **Gotcha:** Upload and modify (file replacement) use `multipart/form-data` with `audio/wav` content type. The SDK handles this via `upload_announcement()` and `modify()`. For raw HTTP, you would need to construct the multipart body manually -- the `rest_put`/`rest_post` JSON helpers do not handle file uploads.
251
+
252
+ ### CLI Examples
253
+
254
+ ```bash
255
+ # List all org-level announcements
256
+ wxcli announcements list
257
+
258
+ # List announcements for a specific location
259
+ wxcli announcements list --location-id Y2lzY29zcGFyazovL_LOC_ID
260
+
261
+ # List all announcements across all locations
262
+ wxcli announcements list --location-id all
263
+
264
+ # Filter by file name or announcement name
265
+ wxcli announcements list --file-name "greeting.wav"
266
+ wxcli announcements list --name "Welcome Greeting"
267
+
268
+ # Get org-level repository usage (total space, limits)
269
+ wxcli announcements show
270
+
271
+ # Get details for an org-level announcement
272
+ wxcli announcements show-announcements-config Y2lzY29zcGFyazovL_ANN_ID
273
+
274
+ # Get details for a location-level announcement
275
+ wxcli announcements show-announcements-locations Y2lzY29zcGFyazovL_LOC_ID Y2lzY29zcGFyazovL_ANN_ID
276
+
277
+ # Get repository usage for a specific location
278
+ wxcli announcements show-usage Y2lzY29zcGFyazovL_LOC_ID
279
+
280
+ # Delete an org-level announcement
281
+ wxcli announcements delete Y2lzY29zcGFyazovL_ANN_ID --force
282
+
283
+ # Delete a location-level announcement
284
+ wxcli announcements delete-announcements Y2lzY29zcGFyazovL_LOC_ID Y2lzY29zcGFyazovL_ANN_ID --force
285
+
286
+ # Update an org-level announcement (metadata via --json-body)
287
+ wxcli announcements update Y2lzY29zcGFyazovL_ANN_ID --json-body '{"name": "Updated Greeting"}'
288
+ ```
289
+
290
+ > **Note:** File upload (creating new announcements) requires `multipart/form-data` which the CLI does not support. Use the SDK `upload_announcement()` method or raw HTTP with `curl` for uploads.
291
+
292
+ ### 1.4 Key Patterns
293
+
294
+ - **Org vs. location scoping:** Every method takes an optional `location_id`. When `None`, the operation targets the org-level repository. When set, it targets that location's repository.
295
+ - **URL pattern:** Org-level uses `/telephony/config/announcements/...`; location-level uses `/telephony/config/locations/{locationId}/announcements/...`.
296
+ - **File format:** WAV audio files. The SDK hard-codes `audio/wav` as the MIME type for uploads.
297
+ - **Checking references before delete:** Use `details()` to inspect `feature_references` and `playlists` before deleting an announcement that may be in use.
298
+
299
+ ---
300
+
301
+ ## 2. Playlists (Music On Hold)
302
+
303
+ Playlists group multiple announcement files for Music On Hold. A playlist can contain up to **25 announcement files** and can be assigned to one or more locations.
304
+
305
+ **SDK access path:** `api.telephony.playlists`
306
+
307
+ **API class:** `PlayListApi` (base: `telephony/config/announcements/playlists`)
308
+
309
+ ### 2.1 Data Models
310
+
311
+ #### `PlayList`
312
+
313
+ | Field | Type | Notes |
314
+ |-------|------|-------|
315
+ | `id` | `str` | Unique identifier |
316
+ | `name` | `str` | Playlist display name |
317
+ | `file_size` | `int` | Total size of files (KB) |
318
+ | `file_count` | `int` | Number of announcements in playlist |
319
+ | `is_in_use` | `bool` | Whether any feature references this playlist |
320
+ | `last_updated` | `datetime` | UTC timestamp |
321
+ | `level` | `str` | Level at which playlist exists |
322
+ | `location_count` | `int` | Number of locations assigned to this playlist |
323
+ | `announcements` | `list[PlaylistAnnouncement]` | Announcement details (populated in `details()`) |
324
+
325
+ #### `PlaylistAnnouncement`
326
+
327
+ | Field | Type | Notes |
328
+ |-------|------|-------|
329
+ | `id` | `str` | Announcement identifier |
330
+ | `name` | `str` | Announcement name |
331
+ | `file_name` | `str` | Uploaded file name |
332
+ | `file_size` | `int` | Size in KB |
333
+ | `media_file_type` | `str` | Media type |
334
+ | `last_updated` | `datetime` | UTC timestamp |
335
+ | `level` | `str` | Announcement level |
336
+
337
+ #### `PlaylistUsage`
338
+
339
+ | Field | Type | Notes |
340
+ |-------|------|-------|
341
+ | `id` | `str` | Playlist identifier |
342
+ | `locations` | `list[PlaylistUsageLocation]` | Locations using this playlist |
343
+
344
+ #### `PlaylistUsageLocation`
345
+
346
+ | Field | Type | Notes |
347
+ |-------|------|-------|
348
+ | `id` | `str` | Location identifier |
349
+ | `name` | `str` | Location name |
350
+ | `feature_reference` | `PlaylistUsageLocationFeatureRef` | Feature referencing the playlist |
351
+
352
+ #### `PlaylistUsageType` (enum)
353
+
354
+ | Value | Description |
355
+ |-------|-------------|
356
+ | `feature` | Filter usage by feature |
357
+ | `location` | Filter usage by location |
358
+
359
+ ### 2.2 Methods
360
+
361
+ #### `list(...)` — List all playlists
362
+
363
+ ```python
364
+ def list(
365
+ self,
366
+ org_id: str = None,
367
+ ) -> list[PlayList]
368
+ ```
369
+
370
+ - Returns a **list** (not a generator) of all playlists in the org.
371
+ - Does **not** use pagination.
372
+
373
+ **Required scope:** `spark-admin:telephony_config_read`
374
+
375
+ #### `create(...)` — Create a new playlist
376
+
377
+ ```python
378
+ def create(
379
+ self,
380
+ name: str, # Unique playlist name
381
+ announcement_ids: list[str], # IDs of announcements to include (max 25)
382
+ org_id: str = None,
383
+ ) -> str # Returns the new playlist ID
384
+ ```
385
+
386
+ - **Max 25 announcements** per playlist.
387
+ - Announcements must already exist in the repository.
388
+
389
+ **Required scope:** `spark-admin:telephony_config_write`
390
+
391
+ #### `details(...)` — Get playlist details
392
+
393
+ ```python
394
+ def details(
395
+ self,
396
+ play_list_id: str,
397
+ org_id: str = None,
398
+ ) -> PlayList
399
+ ```
400
+
401
+ - Returns full details including the `announcements` list.
402
+
403
+ **Required scope:** `spark-admin:telephony_config_read`
404
+
405
+ #### `modify(...)` — Update a playlist
406
+
407
+ ```python
408
+ def modify(
409
+ self,
410
+ play_list_id: str,
411
+ name: str = None, # New name (optional)
412
+ announcement_ids: list[str] = None, # New set of announcement IDs (optional)
413
+ org_id: str = None,
414
+ )
415
+ ```
416
+
417
+ - You can update the name, the announcement list, or both.
418
+
419
+ **Required scope:** `spark-admin:telephony_config_write`
420
+
421
+ #### `delete(...)` — Delete a playlist
422
+
423
+ ```python
424
+ def delete(
425
+ self,
426
+ play_list_id: str,
427
+ org_id: str = None,
428
+ )
429
+ ```
430
+
431
+ **Required scope:** `spark-admin:telephony_config_write`
432
+
433
+ #### `usage(...)` — Get playlist usage
434
+
435
+ ```python
436
+ def usage(
437
+ self,
438
+ play_list_id: str,
439
+ playlist_usage_type: PlaylistUsageType = None, # Filter by 'feature' or 'location'
440
+ ) -> PlaylistUsage
441
+ ```
442
+
443
+ - Returns which locations and features reference this playlist.
444
+
445
+ - **Scopes:** `spark-admin:telephony_config_read` (the `PlayListApi` class docstring confirms all read operations use `spark-admin:telephony_config_read`; the `usage()` method itself has no explicit scope docstring but inherits from the class-level scope)
446
+
447
+ #### `assigned_locations(...)` — List locations assigned to a playlist
448
+
449
+ ```python
450
+ def assigned_locations(
451
+ self,
452
+ play_list_id: str,
453
+ org_id: str = None,
454
+ ) -> list[IdAndName]
455
+ ```
456
+
457
+ - Returns `IdAndName` objects (id + name) for each assigned location.
458
+
459
+ **Required scope:** `spark-admin:telephony_config_read`
460
+
461
+ #### `modify_assigned_locations(...)` — Assign playlist to locations
462
+
463
+ ```python
464
+ def modify_assigned_locations(
465
+ self,
466
+ play_list_id: str,
467
+ location_ids: list[str], # Full list of location IDs to assign
468
+ org_id: str = None,
469
+ )
470
+ ```
471
+
472
+ - This **replaces** the entire location assignment list. To add a location, you must include all existing location IDs plus the new one.
473
+ - Assigning a playlist to a location sets it as that location's Music On Hold source.
474
+
475
+ **Required scope:** `spark-admin:telephony_config_write`
476
+
477
+ ### 2.3 Raw HTTP — Playlists
478
+
479
+ ```python
480
+ BASE = "https://webexapis.com/v1"
481
+
482
+ # GET — List all playlists (org-level, not paginated)
483
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements/playlists")
484
+ # Response: {"playlists": [{"id": "...", "name": "...", "fileSize": 0, "fileCount": 0,
485
+ # "isInUse": false, "lastUpdated": "...", "locationCount": 0}]}
486
+
487
+ # POST — Create a playlist
488
+ body = {
489
+ "name": "Hold Music Rotation",
490
+ "announcements": [{"id": "<ann_id_1>"}, {"id": "<ann_id_2>"}]
491
+ }
492
+ result = api.session.rest_post(f"{BASE}/telephony/config/announcements/playlists", json=body)
493
+ # Returns: {"id": "<playlist_id>"}
494
+
495
+ # GET — Get playlist details (includes announcement list)
496
+ result = api.session.rest_get(f"{BASE}/telephony/config/announcements/playlists/{playlist_id}")
497
+
498
+ # PUT — Update a playlist (name and/or announcements)
499
+ body = {
500
+ "name": "Updated Playlist Name",
501
+ "announcements": [{"id": "<ann_id_1>"}, {"id": "<ann_id_3>"}]
502
+ }
503
+ api.session.rest_put(f"{BASE}/telephony/config/announcements/playlists/{playlist_id}", json=body)
504
+
505
+ # DELETE — Delete a playlist
506
+ api.session.rest_delete(f"{BASE}/telephony/config/announcements/playlists/{playlist_id}")
507
+
508
+ # --- Playlist location assignments ---
509
+
510
+ # GET — List locations assigned to a playlist
511
+ result = api.session.rest_get(
512
+ f"{BASE}/telephony/config/announcements/playlists/{playlist_id}/locations")
513
+ # Response: {"locations": [{"id": "...", "name": "..."}]}
514
+
515
+ # PUT — Assign playlist to locations (full replace)
516
+ body = {
517
+ "locationIds": ["<loc_id_1>", "<loc_id_2>"]
518
+ }
519
+ api.session.rest_put(
520
+ f"{BASE}/telephony/config/announcements/playlists/{playlist_id}/locations", json=body)
521
+ ```
522
+
523
+ **Gotcha:** Location assignment is a full replace, not incremental. Always GET current locations first, then PUT the complete list including any new additions.
524
+ <!-- Updated by playbook session 2026-03-18 -->
525
+
526
+ ### CLI Examples
527
+
528
+ ```bash
529
+ # List all playlists in the org
530
+ wxcli announcement-playlists list
531
+
532
+ # Get details for a specific playlist (includes announcement list)
533
+ wxcli announcement-playlists show Y2lzY29zcGFyazovL_PLAYLIST_ID
534
+
535
+ # Create a playlist (announcement IDs must be passed via --json-body)
536
+ wxcli announcement-playlists create --name "Hold Music Rotation" \
537
+ --json-body '{"name": "Hold Music Rotation", "announcementIds": ["ANN_ID_1", "ANN_ID_2"]}'
538
+
539
+ # Update a playlist name
540
+ wxcli announcement-playlists update Y2lzY29zcGFyazovL_PLAYLIST_ID --name "New Playlist Name"
541
+
542
+ # Update a playlist's announcements via --json-body
543
+ wxcli announcement-playlists update Y2lzY29zcGFyazovL_PLAYLIST_ID \
544
+ --json-body '{"name": "Hold Music Rotation", "announcementIds": ["ANN_ID_1", "ANN_ID_3"]}'
545
+
546
+ # Delete a playlist
547
+ wxcli announcement-playlists delete Y2lzY29zcGFyazovL_PLAYLIST_ID --force
548
+
549
+ # List locations assigned to a playlist
550
+ wxcli announcement-playlists list-playlists Y2lzY29zcGFyazovL_PLAYLIST_ID
551
+
552
+ # Assign a playlist to locations (full replace — include ALL desired location IDs)
553
+ wxcli announcement-playlists update-playlists Y2lzY29zcGFyazovL_PLAYLIST_ID \
554
+ --json-body '{"locationIds": ["LOC_ID_1", "LOC_ID_2"]}'
555
+ ```
556
+
557
+ ### CLI: `cq-playlists` (Call Queue Playlist Usage)
558
+
559
+ The `cq-playlists` CLI group lets you check which call queues or locations reference a specific playlist.
560
+
561
+ | Command | Description |
562
+ |---------|-------------|
563
+ | `cq-playlists list <playlist_id>` | Get playlist usage (which queues/locations reference a playlist) |
564
+
565
+ ```bash
566
+ # See which features use a specific playlist
567
+ wxcli cq-playlists list <playlist_id>
568
+
569
+ # Filter to see only location-level usage
570
+ wxcli cq-playlists list <playlist_id> --playlist-usage-type location
571
+
572
+ # Filter to see only feature-level usage (call queues, auto attendants)
573
+ wxcli cq-playlists list <playlist_id> --playlist-usage-type feature
574
+ ```
575
+
576
+ ### 2.4 Key Patterns
577
+
578
+ - **Playlists are org-level only.** They are created at the org level, then assigned to locations via `modify_assigned_locations()`.
579
+ - **Workflow:** Upload announcements to the repo first, then create a playlist referencing those announcement IDs, then assign the playlist to locations.
580
+ - **Location assignment is a full replace**, not incremental. Always read current assignments with `assigned_locations()` before modifying.
581
+
582
+ ---
583
+
584
+ ## 3. Access Codes
585
+
586
+ Access codes (also called authorization codes) let authorized users bypass outgoing or incoming calling permission restrictions. They exist at two levels: **location** and **organization**.
587
+
588
+ ### 3.1 Location Access Codes
589
+
590
+ **SDK access path:** `api.telephony.access_codes`
591
+
592
+ **API class:** `LocationAccessCodesApi` (base: `telephony/config/locations`)
593
+
594
+ **Endpoint pattern:** `/telephony/config/locations/{locationId}/outgoingPermission/accessCodes`
595
+
596
+ #### Data Model: `AuthCode`
597
+
598
+ | Field | Type | Notes |
599
+ |-------|------|-------|
600
+ | `code` | `str` | The authorization code string |
601
+ | `description` | `str` | Human-readable description |
602
+ | `level` | `AuthCodeLevel` | `LOCATION` or `CUSTOM` (read-only, set by system) |
603
+
604
+ #### Methods
605
+
606
+ ##### `read(...)` — List access codes for a location
607
+
608
+ ```python
609
+ def read(
610
+ self,
611
+ location_id: str,
612
+ org_id: str = None,
613
+ ) -> list[AuthCode]
614
+ ```
615
+
616
+ **Required scope:** `spark-admin:telephony_config_read`
617
+
618
+ ##### `create(...)` — Add access codes to a location
619
+
620
+ ```python
621
+ def create(
622
+ self,
623
+ location_id: str,
624
+ access_codes: list[AuthCode], # One or more codes to add
625
+ org_id: str = None,
626
+ ) -> list[AuthCode]
627
+ ```
628
+
629
+ - Can add multiple codes in a single call.
630
+
631
+ **Required scope:** `spark-admin:telephony_config_write`
632
+
633
+ ##### `delete_codes(...)` — Delete specific access codes
634
+
635
+ ```python
636
+ def delete_codes(
637
+ self,
638
+ location_id: str,
639
+ access_codes: list[Union[str, AuthCode]], # Code strings or AuthCode objects
640
+ org_id: str = None,
641
+ ) -> list[AuthCode]
642
+ ```
643
+
644
+ - Accepts either `AuthCode` objects or plain code strings.
645
+ - Uses a PUT request with `deleteCodes` in the body (not a DELETE endpoint).
646
+
647
+ **Required scope:** `spark-admin:telephony_config_write`
648
+
649
+ ##### `delete_all(...)` — Delete all access codes for a location
650
+
651
+ ```python
652
+ def delete_all(
653
+ self,
654
+ location_id: str,
655
+ org_id: str = None,
656
+ )
657
+ ```
658
+
659
+ - Wipes all codes for the specified location.
660
+
661
+ **Required scope:** `spark-admin:telephony_config_write`
662
+
663
+ ### 3.2 Organization Access Codes
664
+
665
+ **SDK access path:** `api.telephony.org_access_codes`
666
+
667
+ **API class:** `OrganisationAccessCodesApi` (base: `telephony/config/outgoingPermission/accessCodes`)
668
+
669
+ Organization-level access codes apply across all locations in the org.
670
+
671
+ #### Methods
672
+
673
+ ##### `list(...)` — List org-level access codes
674
+
675
+ ```python
676
+ def list(
677
+ self,
678
+ code: list[str] = None, # Filter by code values (comma-separated internally)
679
+ description: list[str] = None, # Filter by descriptions (comma-separated internally)
680
+ org_id: str = None,
681
+ **params
682
+ ) -> Generator[AuthCode, None, None]
683
+ ```
684
+
685
+ - Returns a **paginated generator** (unlike the location-level `read()` which returns a plain list).
686
+ - Supports filtering by code and description.
687
+
688
+ **Required scope:** `spark-admin:telephony_config_read`
689
+
690
+ ##### `create(...)` — Create org-level access codes
691
+
692
+ ```python
693
+ def create(
694
+ self,
695
+ access_codes: list[AuthCode], # Max 10,000 per request
696
+ org_id: str = None,
697
+ )
698
+ ```
699
+
700
+ - Supports up to **10,000 codes per request**.
701
+
702
+ **Required scope:** `spark-admin:telephony_config_write`
703
+
704
+ ##### `delete(...)` — Delete org-level access codes
705
+
706
+ ```python
707
+ def delete(
708
+ self,
709
+ delete_codes: list[str] = None, # Code strings to delete (max 10,000 per request)
710
+ org_id: str = None,
711
+ )
712
+ ```
713
+
714
+ - Supports up to **10,000 codes per request**.
715
+ - Uses a PUT request with `deleteCodes` in the body (same pattern as location-level).
716
+
717
+ **Required scope:** `spark-admin:telephony_config_write`
718
+
719
+ ### 3.3 Raw HTTP — Access Codes
720
+
721
+ ```python
722
+ BASE = "https://webexapis.com/v1"
723
+
724
+ # --- Location-level access codes ---
725
+
726
+ # GET — List access codes for a location
727
+ result = api.session.rest_get(
728
+ f"{BASE}/telephony/config/locations/{loc_id}/outgoingPermission/accessCodes")
729
+ # Response: {"accessCodes": [{"code": "1234", "description": "Sales team", "level": "LOCATION"}]}
730
+
731
+ # POST — Create access codes for a location
732
+ body = {
733
+ "accessCodes": [
734
+ {"code": "1234", "description": "Sales team"},
735
+ {"code": "5678", "description": "Support team"}
736
+ ]
737
+ }
738
+ api.session.rest_post(
739
+ f"{BASE}/telephony/config/locations/{loc_id}/outgoingPermission/accessCodes", json=body)
740
+
741
+ # PUT — Delete specific access codes (uses PUT, not DELETE)
742
+ body = {
743
+ "deleteCodes": ["1234", "5678"]
744
+ }
745
+ api.session.rest_put(
746
+ f"{BASE}/telephony/config/locations/{loc_id}/outgoingPermission/accessCodes", json=body)
747
+
748
+ # DELETE — Delete all access codes for a location
749
+ api.session.rest_delete(
750
+ f"{BASE}/telephony/config/locations/{loc_id}/outgoingPermission/accessCodes")
751
+
752
+ # --- Org-level access codes ---
753
+
754
+ # GET — List org-level access codes (paginated)
755
+ result = api.session.rest_get(
756
+ f"{BASE}/telephony/config/outgoingPermission/accessCodes",
757
+ params={"max": 1000})
758
+ # Response: {"accessCodes": [{"code": "9999", "description": "Global bypass"}]}
759
+
760
+ # POST — Create org-level access codes (max 10,000 per request)
761
+ body = {
762
+ "accessCodes": [
763
+ {"code": "9999", "description": "Global bypass"}
764
+ ]
765
+ }
766
+ api.session.rest_post(f"{BASE}/telephony/config/outgoingPermission/accessCodes", json=body)
767
+
768
+ # PUT — Delete org-level access codes (uses PUT, not DELETE; max 10,000 per request)
769
+ body = {
770
+ "deleteCodes": ["9999"]
771
+ }
772
+ api.session.rest_put(f"{BASE}/telephony/config/outgoingPermission/accessCodes", json=body)
773
+ ```
774
+
775
+ **Gotcha:** Deleting specific codes uses PUT with a `deleteCodes` array in the body, not an HTTP DELETE. This applies at both location and org level. The `DELETE` verb is only used for "delete all" at the location level.
776
+ <!-- Updated by playbook session 2026-03-18 -->
777
+
778
+ ### 3.4 Key Patterns
779
+
780
+ - **Location vs. org level:** Location codes bypass permissions for persons/workspaces at that location only. Org codes apply across all locations.
781
+ - **No update method:** To change an access code, delete the old one and create a new one.
782
+ - **Delete uses PUT:** Both location and org `delete` methods send a PUT with `deleteCodes` in the body, not an HTTP DELETE. The SDK handles this transparently.
783
+ - **Batch limits:** Org-level create and delete support up to 10,000 codes per request (confirmed in SDK docstrings: "max limit is 10k per request"). Location-level create/delete has no documented batch limit in the SDK source -- the `LocationAccessCodesApi.create()` and `delete_codes()` methods accept a `list[AuthCode]` with no stated maximum.
784
+
785
+ ---
786
+
787
+ ## 4. Schedules & Holiday Schedules
788
+
789
+ Schedules define time windows (business hours, holidays) that control call routing behavior for features like Auto Attendants. They can be created at the **location** or **person (user)** level.
790
+
791
+ **SDK access path:** `api.telephony.schedules`
792
+
793
+ **API class:** `ScheduleApi` (base depends on context: `telephony/config/locations` for location schedules, `people` for user schedules)
794
+
795
+ ### 4.1 Data Models
796
+
797
+ #### `ScheduleType` (enum)
798
+
799
+ | Value | String | Description |
800
+ |-------|--------|-------------|
801
+ | `business_hours` | `"businessHours"` | Define operating hours |
802
+ | `holidays` | `"holidays"` | Define exceptions to business hours |
803
+
804
+ #### `ScheduleLevel` (enum)
805
+
806
+ | Value | Description |
807
+ |-------|-------------|
808
+ | `LOCATION` | Location-scoped schedule |
809
+ | `ORGANIZATION` | Org-scoped schedule |
810
+ | `PEOPLE` | User-scoped schedule |
811
+
812
+ #### `Schedule`
813
+
814
+ | Field | Type | Notes |
815
+ |-------|------|-------|
816
+ | `name` | `str` | Schedule display name |
817
+ | `new_name` | `str` | New name (only used in `update()`) |
818
+ | `schedule_id` | `str` | Unique identifier (alias: `id`) |
819
+ | `level` | `ScheduleLevel` | Scope level (returned in user-level listing) |
820
+ | `location_name` | `str` | Location name (returned by `list()` for location schedules) |
821
+ | `location_id` | `str` | Location identifier (returned by `list()` for location schedules) |
822
+ | `schedule_type` | `ScheduleType` | `businessHours` or `holidays` (alias: `type`) |
823
+ | `events` | `list[Event]` | List of events in this schedule |
824
+
825
+ **Convenience constructor:**
826
+
827
+ ```python
828
+ Schedule.business(
829
+ name: str,
830
+ day_start: Union[int, datetime.time] = 9,
831
+ day_end: Union[int, datetime.time] = 17,
832
+ break_start: Union[int, datetime.time] = 12,
833
+ break_end: Union[int, datetime.time] = 13,
834
+ ) -> Schedule
835
+ ```
836
+
837
+ Creates a Mon-Fri business hours schedule with two events per day (morning + afternoon, split by lunch break). Integers are interpreted as hours.
838
+
839
+ #### `Event`
840
+
841
+ | Field | Type | Notes |
842
+ |-------|------|-------|
843
+ | `event_id` | `str` | Unique identifier (alias: `id`) |
844
+ | `name` | `str` | Event name |
845
+ | `new_name` | `str` | New name (only used in updates) |
846
+ | `start_date` | `date` | Start date (required if `all_day_enabled` is set) |
847
+ | `end_date` | `date` | End date (required if `all_day_enabled` is set) |
848
+ | `start_time` | `time` | Start time (required if `all_day_enabled` is false/omitted) |
849
+ | `end_time` | `time` | End time (required if `all_day_enabled` is false/omitted) |
850
+ | `all_day_enabled` | `bool` | True for all-day events (e.g., holidays) |
851
+ | `recurrence` | `Recurrence` | Optional recurrence pattern |
852
+
853
+ **Convenience constructor:**
854
+
855
+ ```python
856
+ Event.day_start_end(
857
+ name: str,
858
+ day: datetime.date,
859
+ start_time: Union[int, datetime.time],
860
+ end_time: Union[int, datetime.time],
861
+ ) -> Event
862
+ ```
863
+
864
+ Creates an event on a specific day with given start/end times and a weekly recurrence on that day of the week.
865
+
866
+ #### `Recurrence`
867
+
868
+ Controls how events repeat. Location and user schedules support different recurrence types:
869
+
870
+ | Field | Type | Supported Level |
871
+ |-------|------|-----------------|
872
+ | `recur_for_ever` | `bool` | User + Location |
873
+ | `recur_end_date` | `date` | User + Location |
874
+ | `recur_end_occurrence` | `int` | User only |
875
+ | `recur_daily` | `RecurDaily` | User only |
876
+ | `recur_weekly` | `RecurWeekly` | User + Location |
877
+ | `recur_yearly_by_date` | `RecurYearlyByDate` | Location only |
878
+ | `recur_yearly_by_day` | `RecurYearlyByDay` | Location only |
879
+
880
+ **Convenience constructor:**
881
+
882
+ ```python
883
+ Recurrence.every_week(day: Union[ScheduleDay, datetime.date]) -> Recurrence
884
+ ```
885
+
886
+ Creates a weekly recurrence for a single day that repeats forever.
887
+
888
+ #### Supporting Recurrence Models
889
+
890
+ - **`RecurWeekly`** — boolean flags for each day of the week + `recur_interval` (weeks between occurrences). Helper: `RecurWeekly.single_day(day, recur_interval=1)`.
891
+ - **`RecurYearlyByDate`** — `day_of_month` (int) + `month` (ScheduleMonth enum). Helper: `RecurYearlyByDate.from_date(date)`.
892
+ - **`RecurYearlyByDay`** — `day` (ScheduleDay) + `week` (ScheduleWeek: FIRST/SECOND/THIRD/FOURTH) + `month` (ScheduleMonth).
893
+ - **`RecurDaily`** — `recur_interval` (int, days between occurrences). User schedules only.
894
+
895
+ ### 4.2 Methods
896
+
897
+ All schedule methods take `obj_id` as the first parameter, which is a **location ID** (for location schedules) or a **user/person ID** (for user schedules).
898
+
899
+ #### `list(...)` — List schedules
900
+
901
+ ```python
902
+ def list(
903
+ self,
904
+ obj_id: str, # Location or user ID
905
+ org_id: str = None,
906
+ schedule_type: ScheduleType = None, # Filter by businessHours or holidays
907
+ name: str = None, # Filter by name
908
+ **params
909
+ ) -> Generator[Schedule, None, None]
910
+ ```
911
+
912
+ - Returns a **paginated generator**.
913
+ - Listing does **not** include events; use `details()` to get events.
914
+
915
+ **Required scope:** Depends on schedule level. The wxc_sdk `ScheduleApi.list()` docstring says `spark-admin:people_read` (this applies when listing **person/user** schedules via `/people/{id}/features/schedules`). For **location** schedules via `/telephony/config/locations/{id}/schedules`, the OpenAPI spec requires `spark-admin:telephony_config_read`.
916
+
917
+ #### `details(...)` — Get schedule details with events
918
+
919
+ ```python
920
+ def details(
921
+ self,
922
+ obj_id: str,
923
+ schedule_type: ScheduleTypeOrStr,
924
+ schedule_id: str,
925
+ org_id: str = None,
926
+ ) -> Schedule
927
+ ```
928
+
929
+ - Returns the full schedule including all `events`.
930
+
931
+ **Required scope:** `spark-admin:telephony_config_read`
932
+
933
+ #### `create(...)` — Create a schedule
934
+
935
+ ```python
936
+ def create(
937
+ self,
938
+ obj_id: str,
939
+ schedule: Schedule, # Must include name, schedule_type, and events
940
+ org_id: str = None,
941
+ ) -> str # Returns the new schedule ID
942
+ ```
943
+
944
+ **Required scope:** `spark-admin:telephony_config_write`
945
+
946
+ #### `update(...)` — Update a schedule
947
+
948
+ ```python
949
+ def update(
950
+ self,
951
+ obj_id: str,
952
+ schedule: Schedule,
953
+ schedule_type: ScheduleTypeOrStr = None, # Default: from schedule object
954
+ schedule_id: str = None, # Default: from schedule object
955
+ org_id: str = None,
956
+ ) -> str # Returns schedule ID (changes if name changed)
957
+ ```
958
+
959
+ > **Important:** The schedule ID **changes** if the schedule name is modified.
960
+
961
+ **Required scope:** `spark-admin:telephony_config_write`
962
+
963
+ #### `delete_schedule(...)` — Delete a schedule
964
+
965
+ ```python
966
+ def delete_schedule(
967
+ self,
968
+ obj_id: str,
969
+ schedule_type: ScheduleTypeOrStr,
970
+ schedule_id: str,
971
+ org_id: str = None,
972
+ )
973
+ ```
974
+
975
+ **Required scope:** `spark-admin:telephony_config_write`
976
+
977
+ #### `event_details(...)` — Get a single event
978
+
979
+ ```python
980
+ def event_details(
981
+ self,
982
+ obj_id: str,
983
+ schedule_type: ScheduleTypeOrStr,
984
+ schedule_id: str,
985
+ event_id: str,
986
+ org_id: str = None,
987
+ ) -> Event
988
+ ```
989
+
990
+ **Required scope:** `spark-admin:telephony_config_read`
991
+
992
+ #### `event_create(...)` — Add an event to an existing schedule
993
+
994
+ ```python
995
+ def event_create(
996
+ self,
997
+ obj_id: str,
998
+ schedule_type: ScheduleTypeOrStr,
999
+ schedule_id: str,
1000
+ event: Event,
1001
+ org_id: str = None,
1002
+ ) -> str # Returns the new event ID
1003
+ ```
1004
+
1005
+ **Required scope:** `spark-admin:telephony_config_write`
1006
+
1007
+ #### `event_update(...)` — Update an event
1008
+
1009
+ ```python
1010
+ def event_update(
1011
+ self,
1012
+ obj_id: str,
1013
+ schedule_type: ScheduleTypeOrStr,
1014
+ schedule_id: str,
1015
+ event: Event,
1016
+ event_id: str = None, # Default: from event object
1017
+ org_id: str = None,
1018
+ ) -> str # Returns event ID (changes if name changed)
1019
+ ```
1020
+
1021
+ > **Important:** The event ID **changes** if the event name is modified.
1022
+
1023
+ **Required scope:** `spark-admin:telephony_config_write`
1024
+
1025
+ #### `event_delete(...)` — Delete an event
1026
+
1027
+ ```python
1028
+ def event_delete(
1029
+ self,
1030
+ obj_id: str,
1031
+ schedule_type: ScheduleTypeOrStr,
1032
+ schedule_id: str,
1033
+ event_id: str,
1034
+ org_id: str = None,
1035
+ )
1036
+ ```
1037
+
1038
+ **Required scope:** `spark-admin:telephony_config_write`
1039
+
1040
+ ### 4.3 Raw HTTP — Schedules
1041
+
1042
+ ```python
1043
+ BASE = "https://webexapis.com/v1"
1044
+
1045
+ # --- Location schedules ---
1046
+
1047
+ # GET — List schedules for a location (paginated)
1048
+ result = api.session.rest_get(
1049
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules",
1050
+ params={"max": 1000, "type": "businessHours"} # or "holidays"
1051
+ )
1052
+ # Response: {"schedules": [{"id": "...", "name": "...", "type": "businessHours"}]}
1053
+ # Note: list does NOT include events — use details to get events
1054
+
1055
+ # POST — Create a schedule
1056
+ body = {
1057
+ "type": "holidays",
1058
+ "name": "National Holidays",
1059
+ "events": [
1060
+ {
1061
+ "name": "Independence Day",
1062
+ "startDate": "2026-07-04",
1063
+ "endDate": "2026-07-04",
1064
+ "allDayEnabled": True
1065
+ }
1066
+ ]
1067
+ }
1068
+ result = api.session.rest_post(f"{BASE}/telephony/config/locations/{loc_id}/schedules", json=body)
1069
+ # Returns: {"id": "<schedule_id>"}
1070
+
1071
+ # GET — Get schedule details (includes events)
1072
+ # sched_type is "businessHours" or "holidays"
1073
+ result = api.session.rest_get(
1074
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}"
1075
+ )
1076
+
1077
+ # PUT — Update a schedule
1078
+ body = {
1079
+ "name": "Updated Schedule Name",
1080
+ "events": [...] # Full event list
1081
+ }
1082
+ result = api.session.rest_put(
1083
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}",
1084
+ json=body
1085
+ )
1086
+ # Returns schedule ID (changes if name changed!)
1087
+
1088
+ # DELETE — Delete a schedule
1089
+ api.session.rest_delete(
1090
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}"
1091
+ )
1092
+
1093
+ # --- Schedule events ---
1094
+
1095
+ # GET — Get a single event
1096
+ result = api.session.rest_get(
1097
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}/events/{event_id}"
1098
+ )
1099
+
1100
+ # POST — Add an event to a schedule
1101
+ body = {
1102
+ "name": "Christmas Day",
1103
+ "startDate": "2026-12-25",
1104
+ "endDate": "2026-12-25",
1105
+ "allDayEnabled": True
1106
+ }
1107
+ result = api.session.rest_post(
1108
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}/events",
1109
+ json=body
1110
+ )
1111
+ # Returns: {"id": "<event_id>"}
1112
+
1113
+ # PUT — Update an event
1114
+ body = {
1115
+ "name": "Christmas Day 2026",
1116
+ "startDate": "2026-12-25",
1117
+ "endDate": "2026-12-25",
1118
+ "allDayEnabled": True,
1119
+ "recurrence": {
1120
+ "recurYearlyByDate": {"dayOfMonth": 25, "month": "DECEMBER"}
1121
+ }
1122
+ }
1123
+ result = api.session.rest_put(
1124
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}/events/{event_id}",
1125
+ json=body
1126
+ )
1127
+ # Returns event ID (changes if name changed!)
1128
+
1129
+ # DELETE — Delete an event
1130
+ api.session.rest_delete(
1131
+ f"{BASE}/telephony/config/locations/{loc_id}/schedules/{sched_type}/{sched_id}/events/{event_id}"
1132
+ )
1133
+
1134
+ # --- User-level schedules (same pattern, different base) ---
1135
+ # Base: {BASE}/people/{person_id}/features/schedules/...
1136
+ ```
1137
+
1138
+ **Gotcha:** Schedule IDs are name-derived (base64-encoded from the schedule name). Renaming a schedule changes its ID -- the old ID returns 404 after rename. Always re-fetch the ID after a name change. Event IDs behave the same way.
1139
+
1140
+ **Gotcha:** The `ScheduleApi` import path is `wxc_sdk.common.schedules`, NOT `wxc_sdk.telephony.schedules`.
1141
+
1142
+ ### CLI Examples
1143
+
1144
+ ```bash
1145
+ # List all schedules for a location
1146
+ wxcli location-schedules list Y2lzY29zcGFyazovL_LOC_ID
1147
+
1148
+ # List only business hours schedules
1149
+ wxcli location-schedules list Y2lzY29zcGFyazovL_LOC_ID --type businessHours
1150
+
1151
+ # List only holiday schedules
1152
+ wxcli location-schedules list Y2lzY29zcGFyazovL_LOC_ID --type holidays
1153
+
1154
+ # Filter by schedule name
1155
+ wxcli location-schedules list Y2lzY29zcGFyazovL_LOC_ID --name "National Holidays"
1156
+
1157
+ # Get schedule details (includes events); type is "businessHours" or "holidays"
1158
+ wxcli location-schedules show Y2lzY29zcGFyazovL_LOC_ID holidays Y2lzY29zcGFyazovL_SCHED_ID
1159
+
1160
+ # Create a holiday schedule (events can be added later)
1161
+ wxcli location-schedules create Y2lzY29zcGFyazovL_LOC_ID \
1162
+ --type holidays --name "National Holidays"
1163
+
1164
+ # Create a business hours schedule with events via --json-body
1165
+ wxcli location-schedules create Y2lzY29zcGFyazovL_LOC_ID \
1166
+ --type businessHours --name "Office Hours" \
1167
+ --json-body '{
1168
+ "type": "businessHours",
1169
+ "name": "Office Hours",
1170
+ "events": [
1171
+ {"name": "Monday AM", "startDate": "2026-01-05", "endDate": "2026-01-05",
1172
+ "startTime": "09:00", "endTime": "12:00"}
1173
+ ]
1174
+ }'
1175
+
1176
+ # Add an all-day holiday event to an existing schedule
1177
+ wxcli location-schedules create-events Y2lzY29zcGFyazovL_LOC_ID holidays Y2lzY29zcGFyazovL_SCHED_ID \
1178
+ --name "Independence Day" --start-date 2026-07-04 --end-date 2026-07-04 --all-day-enabled
1179
+
1180
+ # Add a timed event (business hours block)
1181
+ wxcli location-schedules create-events Y2lzY29zcGFyazovL_LOC_ID businessHours Y2lzY29zcGFyazovL_SCHED_ID \
1182
+ --name "Monday AM" --start-date 2026-01-05 --end-date 2026-01-05 \
1183
+ --start-time "09:00" --end-time "12:00"
1184
+
1185
+ # Get details for a specific event
1186
+ wxcli location-schedules show-events Y2lzY29zcGFyazovL_LOC_ID holidays Y2lzY29zcGFyazovL_SCHED_ID Y2lzY29zcGFyazovL_EVENT_ID
1187
+
1188
+ # Delete an event from a schedule
1189
+ wxcli location-schedules delete-events Y2lzY29zcGFyazovL_LOC_ID holidays Y2lzY29zcGFyazovL_SCHED_ID Y2lzY29zcGFyazovL_EVENT_ID --force
1190
+
1191
+ # Delete an entire schedule
1192
+ wxcli location-schedules delete Y2lzY29zcGFyazovL_LOC_ID holidays Y2lzY29zcGFyazovL_SCHED_ID --force
1193
+ ```
1194
+
1195
+ > **Note:** Schedule and event IDs are name-derived (base64-encoded). Renaming a schedule or event changes its ID -- always re-fetch the ID after a name change.
1196
+
1197
+ ### 4.4 Endpoint URL Structure
1198
+
1199
+ Location schedules:
1200
+ ```
1201
+ /v1/telephony/config/locations/{locationId}/schedules
1202
+ /v1/telephony/config/locations/{locationId}/schedules/{scheduleType}/{scheduleId}
1203
+ /v1/telephony/config/locations/{locationId}/schedules/{scheduleType}/{scheduleId}/events
1204
+ /v1/telephony/config/locations/{locationId}/schedules/{scheduleType}/{scheduleId}/events/{eventId}
1205
+ ```
1206
+
1207
+ User schedules:
1208
+ ```
1209
+ /v1/people/{personId}/features/schedules
1210
+ /v1/people/{personId}/features/schedules/{scheduleType}/{scheduleId}
1211
+ /v1/people/{personId}/features/schedules/{scheduleType}/{scheduleId}/events
1212
+ /v1/people/{personId}/features/schedules/{scheduleType}/{scheduleId}/events/{eventId}
1213
+ ```
1214
+
1215
+ ### 4.4 US Holidays Example (from `examples/us_holidays.py`)
1216
+
1217
+ The SDK repo includes a complete example showing how to programmatically create and maintain a "National Holidays" schedule across all US locations. Key patterns from this example:
1218
+
1219
+ **Full workflow:**
1220
+
1221
+ 1. **Get US locations:** Filter `api.locations.list()` by `address.country == 'US'`.
1222
+ 2. **Get holidays:** Uses the `calendarific` package to fetch US national holidays for a year range.
1223
+ 3. **Check for existing schedule:** List holiday schedules filtered by name `"National Holidays"`.
1224
+ 4. **Create or update:**
1225
+ - If no schedule exists, create a new one with all future holiday events.
1226
+ - If schedule exists, get details (to load events), delete past events, and add missing future events.
1227
+
1228
+ **Key code patterns from the example:**
1229
+
1230
+ ```python
1231
+ # Access the schedules API
1232
+ ats = api.telephony.schedules
1233
+
1234
+ # List holiday schedules for a location, filter by name
1235
+ schedule = next(
1236
+ (s for s in ats.list(
1237
+ obj_id=location.location_id,
1238
+ schedule_type=ScheduleType.holidays,
1239
+ name='National Holidays'
1240
+ ) if s.name == 'National Holidays'),
1241
+ None
1242
+ )
1243
+
1244
+ # Get full details (list doesn't include events)
1245
+ schedule = ats.details(
1246
+ obj_id=location.location_id,
1247
+ schedule_type=ScheduleType.holidays,
1248
+ schedule_id=schedule.schedule_id
1249
+ )
1250
+
1251
+ # Create an all-day holiday event
1252
+ event = Event(
1253
+ name='Independence Day 2026',
1254
+ start_date=date(2026, 7, 4),
1255
+ end_date=date(2026, 7, 4),
1256
+ all_day_enabled=True
1257
+ )
1258
+
1259
+ # Create a new holiday schedule with events
1260
+ schedule = Schedule(
1261
+ name='National Holidays',
1262
+ schedule_type=ScheduleType.holidays,
1263
+ events=[event1, event2, ...]
1264
+ )
1265
+ schedule_id = ats.create(obj_id=location.location_id, schedule=schedule)
1266
+
1267
+ # Add a single event to an existing schedule
1268
+ event_id = ats.event_create(
1269
+ obj_id=location.location_id,
1270
+ schedule_type=ScheduleType.holidays,
1271
+ schedule_id=schedule.schedule_id,
1272
+ event=event
1273
+ )
1274
+
1275
+ # Delete a past event
1276
+ ats.event_delete(
1277
+ obj_id=location.location_id,
1278
+ schedule_type=ScheduleType.holidays,
1279
+ schedule_id=schedule.schedule_id,
1280
+ event_id=event.event_id
1281
+ )
1282
+
1283
+ # Delete an entire holiday schedule
1284
+ ats.delete_schedule(
1285
+ obj_id=location.location_id,
1286
+ schedule_type=ScheduleType.holidays,
1287
+ schedule_id=schedule.schedule_id
1288
+ )
1289
+ ```
1290
+
1291
+ **Threading pattern:** The example uses `ThreadPoolExecutor` with per-location locks to safely update schedules across multiple locations concurrently. Each location gets its own lock to prevent race conditions when multiple years are processed in parallel.
1292
+
1293
+ **Holiday event filtering:** Events are excluded if they fall on a Sunday or are in the past (`holiday.date >= today and holiday.date.weekday() != 6`).
1294
+
1295
+ ---
1296
+
1297
+ ## 5. Cross-Cutting Patterns
1298
+
1299
+ ### Auth Scopes Summary
1300
+
1301
+ | Operation | Required Scope |
1302
+ |-----------|---------------|
1303
+ | Read announcements, playlists, schedules, access codes | `spark-admin:telephony_config_read` |
1304
+ | Write announcements, playlists, schedules, access codes | `spark-admin:telephony_config_write` |
1305
+ | List schedules (location) | `spark-admin:telephony_config_read` |
1306
+ | List schedules (user/person) | `spark-admin:people_read` |
1307
+
1308
+ ### Org vs. Location Scoping
1309
+
1310
+ | Resource | Org-Level | Location-Level |
1311
+ |----------|-----------|----------------|
1312
+ | Announcements | Yes (default when `location_id` is None) | Yes (pass `location_id`) |
1313
+ | Playlists | Yes (created at org, assigned to locations) | No (assigned via `modify_assigned_locations`) |
1314
+ | Access Codes | Yes (`OrganisationAccessCodesApi`) | Yes (`LocationAccessCodesApi`) |
1315
+ | Schedules | No | Yes (primary), also user-level |
1316
+
1317
+ ### Typical Music On Hold Setup Workflow
1318
+
1319
+ 1. Upload WAV files to the announcement repository (org or location level).
1320
+ 2. Create a playlist referencing the uploaded announcement IDs (max 25).
1321
+ 3. Assign the playlist to target locations via `modify_assigned_locations()`.
1322
+
1323
+ ### Typical Holiday Schedule Workflow
1324
+
1325
+ 1. Identify target locations (e.g., filter by country).
1326
+ 2. Check if a holiday schedule already exists for each location.
1327
+ 3. Create the schedule with all-day events, or add events to an existing schedule.
1328
+ 4. Periodically clean up past events and add future ones.
1329
+
1330
+ ---
1331
+
1332
+ ## See Also
1333
+
1334
+ - **[Call Features — Major](call-features-major.md)** — Auto Attendants, Call Queues, and Hunt Groups that consume schedules for time-based routing