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,1229 @@
1
+ # wxc_sdk Patterns & Recipes
2
+
3
+ Reference document for the `wxc_sdk` Python SDK -- the primary tool for automating Webex Calling administration via the Webex REST APIs.
4
+
5
+ **SDK Version (at time of writing):** `1.30.0`
6
+
7
+ ## Sources
8
+
9
+ - **wxc_sdk v1.30.0** — [github.com/jeokrohn/wxc_sdk](https://github.com/jeokrohn/wxc_sdk)
10
+ - **Docs** — [wxc-sdk.readthedocs.io](http://wxc-sdk.readthedocs.io)
11
+ - **Webex API Reference** — [developer.webex.com](https://developer.webex.com/docs/api/getting-started)
12
+
13
+ ## Required Scopes
14
+
15
+ Token type and scope requirements for common SDK operations. Admin tokens (`spark-admin:*`) cover org-wide management; user tokens (`spark:*`) cover personal settings and call control.
16
+
17
+ | Operation Category | Required Scope | Token Type | Notes |
18
+ |-------------------|----------------|------------|-------|
19
+ | List/read people | `spark-admin:people_read` | Admin / Service App | `calling_data=True` requires this scope |
20
+ | Create/update/delete people | `spark-admin:people_write` | Admin / Service App | Also needs `_read` for pre-flight checks |
21
+ | List/read locations | `spark-admin:locations_read` | Admin / Service App | |
22
+ | Manage locations | `spark-admin:locations_write` | Admin / Service App | |
23
+ | Read telephony config (queues, HG, AA, etc.) | `spark-admin:telephony_config_read` | Admin / Service App | Covers all `/telephony/config/*` endpoints |
24
+ | Write telephony config | `spark-admin:telephony_config_write` | Admin / Service App | Create/update/delete calling features |
25
+ | Read person call settings | `spark-admin:people_read` | Admin / Service App | Voicemail, forwarding, caller ID, etc. |
26
+ | Write person call settings | `spark-admin:people_write` | Admin / Service App | |
27
+ | Read workspace settings | `spark-admin:workspaces_read` | Admin / Service App | |
28
+ | Manage workspaces | `spark-admin:workspaces_write` | Admin / Service App | |
29
+ | Read devices | `spark-admin:devices_read` | Admin / Service App | |
30
+ | Manage devices | `spark-admin:devices_write` | Admin / Service App | |
31
+ | Read licenses | `spark-admin:licenses_read` | Admin / Service App | |
32
+ | Call control (dial, answer, hold, transfer) | `spark:calls_write` | **User** | Must be calling-licensed user's token; admin tokens get 400 |
33
+ | Read own call settings (`/people/me/*`) | `spark:people_read` | **User** | User must have Webex Calling license |
34
+ | CDR / call detail records | `spark-admin:cdr_read` | Admin / Service App | |
35
+ | Webhooks | `spark:webhooks_read`, `spark:webhooks_write` | Admin or User | |
36
+ | Admin audit events | `spark-admin:audit_events_read` | Admin / Service App | |
37
+ | Org/roles/authorizations | `spark-admin:organizations_read` | Admin / Service App | |
38
+
39
+ **Scope naming pattern:** `spark-admin:` prefix = org-wide admin scope; `spark:` prefix = user-level scope. Most SDK operations use admin scopes. Call control and `/people/me/*` endpoints are the main exceptions requiring user-level tokens.
40
+
41
+ ## Table of Contents
42
+
43
+ 1. [Installation](#1-installation)
44
+ 2. [Raw HTTP Pattern](#15-raw-http-pattern-preferred-execution-method)
45
+ 3. [WebexSimpleApi -- The Sync Entry Point](#2-webexsimpleapi----the-sync-entry-point)
46
+ 4. [Authentication Patterns](#3-authentication-patterns)
47
+ 5. [Sync vs Async](#4-sync-vs-async)
48
+ 6. [Common Recipes](#5-common-recipes)
49
+ 7. [Error Handling](#6-error-handling)
50
+ 8. [REST Client Details](#7-rest-client-details)
51
+ 9. [Data Types](#8-data-types)
52
+ 10. [Environment Variable Patterns](#9-environment-variable-patterns)
53
+ 11. [Logging](#10-logging)
54
+ 12. [Key Gotchas](#11-key-gotchas)
55
+ 13. [See Also](#see-also)
56
+
57
+ ---
58
+
59
+ ## 1. Installation
60
+
61
+ ```bash
62
+ # Install
63
+ pip install wxc-sdk
64
+
65
+ # Upgrade
66
+ pip install wxc-sdk --upgrade
67
+ ```
68
+
69
+ Common companion packages used in the examples:
70
+
71
+ ```bash
72
+ pip install python-dotenv # .env file loading
73
+ pip install pyyaml # token caching to YML
74
+ ```
75
+
76
+ Core dependencies pulled in automatically by wxc_sdk:
77
+ - `requests` (sync HTTP -- powers `RestSession`)
78
+ - `aiohttp` (async HTTP -- powers `AsRestSession`)
79
+ - `pydantic` (data models)
80
+ - `aenum` (dynamic enum extension)
81
+ - `python-dateutil` / `pytz` (date handling)
82
+
83
+ ---
84
+
85
+ ## 1.5. Raw HTTP Pattern (Preferred Execution Method)
86
+ <!-- Updated by playbook session 2026-03-18 -->
87
+
88
+ The primary way to execute Webex API calls in this project is **raw HTTP via `api.session.rest_*()`**. We use `wxc_sdk` for authentication and session management only -- all actual API calls go through the REST client methods with full URLs.
89
+
90
+ ### Why Raw HTTP
91
+
92
+ - **Full control** over request/response -- no SDK model translation surprises
93
+ - **Matches the Webex API docs exactly** -- URLs, params, and body keys are 1:1 with developer.webex.com
94
+ - **No SDK version dependency** for API coverage -- new endpoints work immediately
95
+ - **Responses are plain dicts** -- easy to inspect, log, and forward
96
+
97
+ ### Setup
98
+
99
+ ```python
100
+ from wxc_sdk import WebexSimpleApi
101
+
102
+ api = WebexSimpleApi() # auth via WEBEX_ACCESS_TOKEN env var
103
+ BASE = "https://webexapis.com/v1"
104
+ ```
105
+
106
+ ### REST Methods
107
+
108
+ | Method | Signature | Returns |
109
+ |--------|-----------|---------|
110
+ | `rest_get` | `api.session.rest_get(url, params=dict)` | Parsed JSON dict |
111
+ | `rest_post` | `api.session.rest_post(url, json=dict)` | Parsed JSON dict |
112
+ | `rest_put` | `api.session.rest_put(url, json=dict)` | Parsed JSON dict (or None) |
113
+ | `rest_delete` | `api.session.rest_delete(url)` | None |
114
+
115
+ All methods require **full URLs** (`https://webexapis.com/v1/...`), not relative paths.
116
+
117
+ ### CRUD Example
118
+
119
+ ```python
120
+ from wxc_sdk import WebexSimpleApi
121
+ from wxc_sdk.rest import RestError
122
+
123
+ api = WebexSimpleApi()
124
+ BASE = "https://webexapis.com/v1"
125
+
126
+ # ── LIST (GET with params) ────────────────────────────────────────
127
+ result = api.session.rest_get(f"{BASE}/people", params={"max": 1000})
128
+ people = result.get("items", [])
129
+
130
+ # ── CREATE (POST with JSON body) ──────────────────────────────────
131
+ body = {
132
+ "emails": ["jsmith@example.com"],
133
+ "displayName": "John Smith",
134
+ "firstName": "John",
135
+ "lastName": "Smith",
136
+ }
137
+ result = api.session.rest_post(f"{BASE}/people", json=body)
138
+ person_id = result["id"]
139
+
140
+ # ── READ (GET by ID) ──────────────────────────────────────────────
141
+ person = api.session.rest_get(f"{BASE}/people/{person_id}")
142
+
143
+ # ── UPDATE (PUT with full body) ───────────────────────────────────
144
+ person["department"] = "Engineering"
145
+ api.session.rest_put(f"{BASE}/people/{person_id}", json=person)
146
+
147
+ # ── DELETE ─────────────────────────────────────────────────────────
148
+ api.session.rest_delete(f"{BASE}/people/{person_id}")
149
+ ```
150
+
151
+ ### Pagination
152
+
153
+ There is **no automatic pagination** with raw HTTP. The SDK's `follow_pagination()` only works with the typed API methods (e.g., `api.people.list()`). For raw HTTP, request the maximum page size and handle paging manually if needed:
154
+
155
+ ```python
156
+ # Request max page size (API maximum is typically 1000)
157
+ result = api.session.rest_get(f"{BASE}/people", params={"max": 1000})
158
+ items = result.get("items", [])
159
+
160
+ # For endpoints that may return more than 1000 items,
161
+ # check for a next link (rare in practice for most Webex APIs):
162
+ # The SDK does NOT auto-follow next links in raw HTTP mode.
163
+ ```
164
+
165
+ ### Error Handling
166
+
167
+ All `rest_*()` methods raise `RestError` on HTTP errors. The exception carries the full error detail from the Webex API:
168
+
169
+ ```python
170
+ from wxc_sdk.rest import RestError
171
+
172
+ try:
173
+ api.session.rest_get(f"{BASE}/people/invalid-id")
174
+ except RestError as e:
175
+ print(f"HTTP {e.response.status_code}") # e.g., 404
176
+ print(f"Error code: {e.code}") # Webex error code
177
+ print(f"Description: {e.description}") # Human-readable message
178
+ print(f"Tracking ID: {e.detail.tracking_id}") # For Cisco TAC
179
+ ```
180
+
181
+ ### Response Parsing
182
+
183
+ Responses are already-parsed JSON dicts. The response key varies by endpoint:
184
+
185
+ | Endpoint | List response key |
186
+ |----------|------------------|
187
+ | `/v1/people` | `items` |
188
+ | `/v1/locations` | `items` |
189
+ | `/v1/licenses` | `items` |
190
+ | `/v1/workspaces` | `items` |
191
+ | `/v1/telephony/config/numbers` | `phoneNumbers` |
192
+ | `/v1/telephony/config/jobs/numbers/manageNumbers` | `items` |
193
+
194
+ Single-resource GETs return the resource dict directly (no wrapper key).
195
+
196
+ ### When to Use Typed SDK Methods Instead
197
+
198
+ The typed SDK API (e.g., `api.people.list()`) is still useful for:
199
+ - **Auto-pagination** -- `list()` returns generators that follow all pages automatically
200
+ - **Pydantic models** -- typed access with IDE completion and validation
201
+ - **Async bulk operations** -- `AsWebexSimpleApi` with `asyncio.gather` for high-concurrency reads
202
+
203
+ The existing SDK patterns in this doc remain valid. Raw HTTP is the **preferred approach for write operations and targeted reads** where you want direct control over the request.
204
+
205
+ ---
206
+
207
+ ## 2. WebexSimpleApi -- The Sync Entry Point
208
+
209
+ `WebexSimpleApi` is the main class. It creates a `RestSession` and attaches every sub-API as a property.
210
+
211
+ ### Constructor
212
+
213
+ ```python
214
+ from wxc_sdk import WebexSimpleApi
215
+
216
+ api = WebexSimpleApi(
217
+ tokens=None, # str | Tokens | None
218
+ concurrent_requests=10, # max parallel threads (Semaphore)
219
+ retry_429=True, # auto-retry on rate limit
220
+ # session=RestSession(...) # pass your own session (advanced)
221
+ )
222
+ ```
223
+
224
+ **Token resolution order:**
225
+ 1. If `tokens` is a `str`, it is wrapped in `Tokens(access_token=tokens)`.
226
+ 2. If `tokens` is a `Tokens` instance, it is used directly.
227
+ 3. If `tokens` is `None`, the SDK reads `WEBEX_ACCESS_TOKEN` from the environment.
228
+ 4. If the environment variable is also missing, a `ValueError` is raised.
229
+
230
+ ### Context Manager
231
+
232
+ `WebexSimpleApi` supports `with` statements. The `__exit__` method calls `session.close()`:
233
+
234
+ ```python
235
+ with WebexSimpleApi() as api:
236
+ users = list(api.people.list())
237
+ # session is closed here
238
+ ```
239
+
240
+ ### Top-Level API Properties
241
+
242
+ Every property below is an API sub-object. The ones most relevant to Webex Calling are marked with **(WxC)**.
243
+
244
+ | Property | Class | Notes |
245
+ |----------|-------|-------|
246
+ | `api.people` | `PeopleApi` | **(WxC)** List/get users, `calling_data=True` |
247
+ | `api.locations` | `LocationsApi` | **(WxC)** List/manage locations |
248
+ | `api.telephony` | `TelephonyApi` | **(WxC)** Main Calling API -- call queues, hunt groups, auto attendants, schedules, phone numbers, devices, permissions, virtual lines, etc. |
249
+ | `api.person_settings` | `PersonSettingsApi` | **(WxC)** Per-user calling settings -- voicemail, forwarding, devices, caller ID, etc. |
250
+ | `api.workspace_settings` | `WorkspaceSettingsApi` | **(WxC)** Per-workspace calling settings |
251
+ | `api.workspaces` | `WorkspacesApi` | **(WxC)** Workspace CRUD |
252
+ | `api.devices` | `DevicesApi` | **(WxC)** Device management |
253
+ | `api.licenses` | `LicensesApi` | **(WxC)** License inventory |
254
+ | `api.jobs` | `JobsApi` | **(WxC)** Bulk provisioning jobs |
255
+ | `api.workspace_locations` | `WorkspaceLocationApi` | Workspace locations |
256
+ | `api.workspace_personalization` | `WorkspacePersonalizationApi` | Workspace personalization |
257
+ | `api.admin_audit` | `AdminAuditEventsApi` | Audit trail |
258
+ | `api.authorizations` | `AuthorizationsApi` | User authorizations/tokens |
259
+ | `api.cdr` | `DetailedCDRApi` | Call detail records |
260
+ | `api.converged_recordings` | `ConvergedRecordingsApi` | Recordings |
261
+ | `api.device_configurations` | `DeviceConfigurationsApi` | Device configs |
262
+ | `api.events` | `EventsApi` | Webhooks events |
263
+ | `api.groups` | `GroupsApi` | Groups |
264
+ | `api.guests` | `GuestManagementApi` | Guest management |
265
+ | `api.me` | `MeSettingsApi` | Calling settings for authenticated user |
266
+ | `api.meetings` | `MeetingsApi` | Meetings |
267
+ | `api.membership` | `MembershipApi` | Space memberships |
268
+ | `api.messages` | `MessagesApi` | Messages |
269
+ | `api.org_contacts` | `OrganizationContactsApi` | Org contacts |
270
+ | `api.organizations` | `OrganizationApi` | Org settings |
271
+ | `api.reports` | `ReportsApi` | Reports |
272
+ | `api.roles` | `RolesApi` | Roles |
273
+ | `api.rooms` | `RoomsApi` | Spaces/rooms |
274
+ | `api.room_tabs` | `RoomTabsApi` | Room tabs |
275
+ | `api.scim` | `ScimV2Api` | SCIM v2 |
276
+ | `api.status` | `StatusAPI` | Webex status |
277
+ | `api.teams` | `TeamsApi` | Teams |
278
+ | `api.team_memberships` | `TeamMembershipsApi` | Team memberships |
279
+ | `api.webhook` | `WebhookApi` | Webhooks |
280
+ | `api.xapi` | `XApi` | xAPI |
281
+
282
+ ### Telephony Sub-APIs (via `api.telephony.*`)
283
+
284
+ The `TelephonyApi` object is the gateway to all calling features. Key sub-properties:
285
+
286
+ - `api.telephony.access_codes` -- Location access/authentication codes
287
+ - `api.telephony.announcements_repo` -- Announcements repository
288
+ - `api.telephony.auto_attendant` -- Auto attendant CRUD
289
+ - `api.telephony.call_controls_members` -- Call controls members
290
+ - `api.telephony.call_intercept` -- Location call intercept settings
291
+ - `api.telephony.call_recording` -- Call recording settings
292
+ - `api.telephony.call_routing` -- Call routing
293
+ - `api.telephony.caller_reputation_provider` -- Caller reputation provider
294
+ - `api.telephony.calls` -- Calls API
295
+ - `api.telephony.callpark` -- Call park CRUD
296
+ - `api.telephony.callpark_extension` -- Call park extensions
297
+ - `api.telephony.callqueue` -- Call queue CRUD, agents
298
+ - `api.telephony.conference` -- Conference controls
299
+ - `api.telephony.cx_essentials` -- Customer Assist (formerly CX Essentials)
300
+ - `api.telephony.dect_devices` -- DECT device management
301
+ - `api.telephony.devices` -- Telephony device details, members, MAC validation
302
+ - `api.telephony.emergency_address` -- Emergency address management
303
+ - `api.telephony.emergency_services` -- Org emergency services
304
+ - `api.telephony.guest_calling` -- Guest calling
305
+ - `api.telephony.hotdesk` -- Hot desk management
306
+ - `api.telephony.hotdesking_voiceportal` -- Hot desking sign-in via voice portal
307
+ - `api.telephony.huntgroup` -- Hunt group CRUD
308
+ - `api.telephony.jobs` -- Bulk provisioning jobs
309
+ - `api.telephony.location` -- Location-level telephony settings
310
+ - `api.telephony.locations` -- Alias for `location` (same object)
311
+ - `api.telephony.ms_teams` -- Org MS Teams settings
312
+ - `api.telephony.operating_modes` -- Operating modes
313
+ - `api.telephony.organisation_access_codes` -- Org-level access codes
314
+ - `api.telephony.organisation_voicemail` -- Org voicemail settings
315
+ - `api.telephony.paging` -- Paging groups
316
+ - `api.telephony.permissions_out` -- Outgoing call permissions
317
+ - `api.telephony.phone_numbers(...)` -- List phone numbers org-wide or per-location (method, not sub-API)
318
+ - `api.telephony.pickup` -- Call pickup CRUD
319
+ - `api.telephony.playlist` -- Playlists (music on hold)
320
+ - `api.telephony.pnc` -- Private network connect
321
+ - `api.telephony.prem_pstn` -- Premises PSTN
322
+ - `api.telephony.pstn` -- PSTN settings
323
+ - `api.telephony.schedules` -- Holiday/business hours schedules
324
+ - `api.telephony.supervisors` -- Supervisor management
325
+ - `api.telephony.virtual_extensions` -- Virtual extensions
326
+ - `api.telephony.virtual_lines` -- Virtual line management
327
+ - `api.telephony.voicemail_groups` -- Voicemail groups
328
+ - `api.telephony.voicemail_rules` -- Voicemail rules
329
+ - `api.telephony.voice_messaging` -- Voice messaging
330
+ - `api.telephony.voiceportal` -- Voice portal settings
331
+
332
+ ---
333
+
334
+ ## 3. Authentication Patterns
335
+
336
+ ### Pattern A: Environment Variable (Simplest)
337
+
338
+ Set `WEBEX_ACCESS_TOKEN` and construct with no arguments. Best for quick scripts and developer tokens.
339
+
340
+ ```python
341
+ from dotenv import load_dotenv
342
+ from wxc_sdk import WebexSimpleApi
343
+
344
+ load_dotenv(override=True) # reads .env file
345
+
346
+ api = WebexSimpleApi()
347
+ me = api.people.me()
348
+ print(f'Authenticated as {me.display_name}')
349
+ ```
350
+
351
+ **.env file:**
352
+ ```
353
+ WEBEX_ACCESS_TOKEN=NjY2Yz...
354
+ ```
355
+
356
+ ### Pattern B: Token String Directly
357
+
358
+ ```python
359
+ api = WebexSimpleApi(tokens='NjY2Yz...')
360
+ ```
361
+
362
+ ### Pattern C: OAuth Integration (Interactive)
363
+
364
+ For production scripts that need token refresh. Uses `wxc_sdk.integration.Integration` to run a local OAuth flow and cache tokens to a YML file.
365
+
366
+ ```python
367
+ import os
368
+ from wxc_sdk import WebexSimpleApi
369
+ from wxc_sdk.integration import Integration
370
+ from wxc_sdk.scopes import parse_scopes
371
+ from wxc_sdk.tokens import Tokens
372
+
373
+ def build_integration() -> Integration:
374
+ client_id = os.getenv('TOKEN_INTEGRATION_CLIENT_ID')
375
+ client_secret = os.getenv('TOKEN_INTEGRATION_CLIENT_SECRET')
376
+ scopes = parse_scopes(os.getenv('TOKEN_INTEGRATION_CLIENT_SCOPES'))
377
+ redirect_url = 'http://localhost:6001/redirect'
378
+ return Integration(
379
+ client_id=client_id,
380
+ client_secret=client_secret,
381
+ scopes=scopes,
382
+ redirect_url=redirect_url,
383
+ )
384
+
385
+ def get_tokens() -> Tokens:
386
+ integration = build_integration()
387
+ # reads from YML cache; if expired, opens browser for OAuth
388
+ tokens = integration.get_cached_tokens_from_yml(yml_path='tokens.yml')
389
+ return tokens
390
+
391
+ tokens = get_tokens()
392
+ api = WebexSimpleApi(tokens=tokens)
393
+ ```
394
+
395
+ **Required .env:**
396
+ ```
397
+ TOKEN_INTEGRATION_CLIENT_ID=C1234...
398
+ TOKEN_INTEGRATION_CLIENT_SECRET=abc123...
399
+ TOKEN_INTEGRATION_CLIENT_SCOPES="spark-admin:telephony_config_read spark-admin:people_read"
400
+ ```
401
+
402
+ ### Pattern D: Service App (Headless / Production)
403
+
404
+ Service apps use a refresh token + client credentials to obtain access tokens without user interaction. Best for automation and scheduled jobs.
405
+
406
+ ```python
407
+ import os
408
+ from wxc_sdk import WebexSimpleApi
409
+ from wxc_sdk.integration import Integration
410
+ from wxc_sdk.tokens import Tokens
411
+ import yaml
412
+
413
+ SERVICE_APP_ENVS = (
414
+ 'SERVICE_APP_REFRESH_TOKEN',
415
+ 'SERVICE_APP_CLIENT_ID',
416
+ 'SERVICE_APP_CLIENT_SECRET',
417
+ )
418
+
419
+ def get_tokens() -> Tokens:
420
+ refresh, client_id, client_secret = (
421
+ os.getenv(env) for env in SERVICE_APP_ENVS
422
+ )
423
+ # Try reading cached tokens from YML
424
+ yml_path = f'tokens_{client_id}.yml'
425
+ if os.path.isfile(yml_path):
426
+ with open(yml_path) as f:
427
+ tokens = Tokens.model_validate(yaml.safe_load(f))
428
+ # Refresh if less than 24 hours remaining
429
+ if tokens.remaining >= 24 * 60 * 60:
430
+ return tokens
431
+
432
+ # Get fresh tokens using refresh token
433
+ tokens = Tokens(refresh_token=refresh)
434
+ integration = Integration(
435
+ client_id=client_id,
436
+ client_secret=client_secret,
437
+ scopes=[],
438
+ redirect_url=None,
439
+ )
440
+ integration.refresh(tokens=tokens)
441
+
442
+ # Cache to file
443
+ with open(yml_path, mode='w') as f:
444
+ yaml.safe_dump(tokens.model_dump(exclude_none=True), f)
445
+ return tokens
446
+
447
+ tokens = get_tokens()
448
+ with WebexSimpleApi(tokens=tokens) as api:
449
+ users = list(api.people.list())
450
+ print(f'{len(users)} users')
451
+ ```
452
+
453
+ **Required .env:**
454
+ ```
455
+ SERVICE_APP_CLIENT_ID=C5678...
456
+ SERVICE_APP_CLIENT_SECRET=xyz789...
457
+ SERVICE_APP_REFRESH_TOKEN=MmI3Zj...
458
+ ```
459
+
460
+ ### Tokens Class Reference
461
+
462
+ `wxc_sdk.tokens.Tokens` (Pydantic BaseModel):
463
+
464
+ | Field | Type | Notes |
465
+ |-------|------|-------|
466
+ | `access_token` | `Optional[str]` | The bearer token |
467
+ | `expires_in` | `Optional[int]` | Lifetime in seconds at creation time |
468
+ | `expires_at` | `Optional[datetime]` | Computed expiration datetime |
469
+ | `refresh_token` | `Optional[str]` | For refresh flow |
470
+ | `refresh_token_expires_in` | `Optional[int]` | Refresh token lifetime |
471
+ | `refresh_token_expires_at` | `Optional[datetime]` | Refresh token expiration |
472
+ | `token_type` | `Optional[Literal['Bearer']]` | Always "Bearer" |
473
+ | `scope` | `Optional[str]` | Space-delimited scopes |
474
+
475
+ Key property: `tokens.remaining` returns seconds until access_token expires.
476
+
477
+ ---
478
+
479
+ ## 4. Sync vs Async
480
+
481
+ The SDK provides parallel sync and async APIs with identical method signatures.
482
+
483
+ | | Sync | Async |
484
+ |---|---|---|
485
+ | **Class** | `WebexSimpleApi` | `AsWebexSimpleApi` |
486
+ | **Import** | `from wxc_sdk import WebexSimpleApi` | `from wxc_sdk.as_api import AsWebexSimpleApi` |
487
+ | **HTTP lib** | `requests` (`RestSession`) | `aiohttp` (`AsRestSession`) |
488
+ | **Context mgr** | `with api:` | `async with api:` |
489
+ | **Concurrency** | `ThreadPoolExecutor` + Semaphore | `asyncio.gather` + Semaphore |
490
+ | **Pagination** | Generator (`yield`) | AsyncGenerator (`async for`) |
491
+
492
+ ### When to Use Each
493
+
494
+ **Use sync (`WebexSimpleApi`)** when:
495
+ - Writing simple scripts (list users, read a setting)
496
+ - Your script does a handful of sequential API calls
497
+ - You want the simplest possible code
498
+
499
+ **Use async (`AsWebexSimpleApi`)** when:
500
+ - You need to make many API calls (bulk operations)
501
+ - You want parallel execution without threads
502
+ - Performance matters (async is significantly faster for bulk work)
503
+
504
+ ### Async Basic Pattern
505
+
506
+ ```python
507
+ import asyncio
508
+ from wxc_sdk.as_api import AsWebexSimpleApi
509
+
510
+ async def main():
511
+ async with AsWebexSimpleApi(concurrent_requests=40) as api:
512
+ # list calls are awaitable
513
+ users = [user for user in await api.people.list(calling_data=True)
514
+ if user.location_id]
515
+
516
+ # parallel detail fetches with asyncio.gather
517
+ details = await asyncio.gather(
518
+ *[api.people.details(person_id=u.person_id, calling_data=True)
519
+ for u in users]
520
+ )
521
+ print(f'Got details for {len(details)} users')
522
+
523
+ asyncio.run(main())
524
+ ```
525
+
526
+ ### Async Generator Pattern
527
+
528
+ Some async list methods return `AsyncGenerator` via `list_gen()`:
529
+
530
+ ```python
531
+ async with AsWebexSimpleApi(concurrent_requests=40) as api:
532
+ calling_license_ids = set(
533
+ lic.license_id for lic in await api.licenses.list()
534
+ if lic.webex_calling
535
+ )
536
+ # list_gen() returns an async generator
537
+ calling_users = [
538
+ user async for user in api.people.list_gen()
539
+ if any(lid in calling_license_ids for lid in user.licenses)
540
+ ]
541
+ ```
542
+
543
+ ### Side-by-Side Comparison: Sync vs Async
544
+
545
+ **Sync (ThreadPoolExecutor):**
546
+ ```python
547
+ from concurrent.futures import ThreadPoolExecutor
548
+ from wxc_sdk import WebexSimpleApi
549
+
550
+ with WebexSimpleApi(concurrent_requests=5) as api:
551
+ locations = [loc for loc in api.locations.list()
552
+ if loc.address.country == 'US']
553
+ with ThreadPoolExecutor() as pool:
554
+ list(pool.map(
555
+ lambda loc: do_something(api=api, location=loc),
556
+ locations
557
+ ))
558
+ ```
559
+
560
+ **Async (asyncio.gather):**
561
+ ```python
562
+ import asyncio
563
+ from wxc_sdk.as_api import AsWebexSimpleApi
564
+
565
+ async def main():
566
+ async with AsWebexSimpleApi(concurrent_requests=5) as api:
567
+ locations = [loc for loc in await api.locations.list()
568
+ if loc.address.country == 'US']
569
+ await asyncio.gather(
570
+ *[do_something(api=api, location=loc) for loc in locations]
571
+ )
572
+
573
+ asyncio.run(main())
574
+ ```
575
+
576
+ ---
577
+
578
+ ## 5. Common Recipes
579
+
580
+ ### 5.1 List All Calling-Enabled Users
581
+
582
+ The simplest and most common operation. A user is calling-enabled if `location_id` is set.
583
+
584
+ ```python
585
+ from dotenv import load_dotenv
586
+ from wxc_sdk import WebexSimpleApi
587
+
588
+ load_dotenv(override=True)
589
+ api = WebexSimpleApi()
590
+
591
+ calling_users = [user for user in api.people.list(calling_data=True)
592
+ if user.location_id]
593
+ print(f'{len(calling_users)} calling users')
594
+ for user in calling_users:
595
+ print(f' {user.display_name} ({user.emails[0]})')
596
+ ```
597
+
598
+ **Key detail:** `calling_data=True` is required to populate `location_id` and other calling-specific fields. Without it, `location_id` will always be `None`.
599
+
600
+ ### 5.2 Filter Users by Location
601
+
602
+ ```python
603
+ # Get all locations, find target
604
+ locations = list(api.locations.list())
605
+ target = next(loc for loc in locations if loc.name == 'San Jose')
606
+
607
+ # Get calling users in that location
608
+ calling_users = [user for user in api.people.list(calling_data=True)
609
+ if user.location_id == target.location_id]
610
+ ```
611
+
612
+ ### 5.3 Identify Calling Users via License (Async Approach)
613
+
614
+ An alternative to `calling_data=True` that avoids the slower per-user calling data fetch:
615
+
616
+ ```python
617
+ async with AsWebexSimpleApi(concurrent_requests=40) as api:
618
+ calling_license_ids = set(
619
+ lic.license_id for lic in await api.licenses.list()
620
+ if lic.webex_calling
621
+ )
622
+ calling_users = [
623
+ user async for user in api.people.list_gen()
624
+ if any(lid in calling_license_ids for lid in user.licenses)
625
+ ]
626
+ ```
627
+
628
+ ### 5.4 Bulk Read Person Settings (Async)
629
+
630
+ Reading voicemail, forwarding, or device settings for many users in parallel:
631
+
632
+ ```python
633
+ import asyncio
634
+ from wxc_sdk.as_api import AsWebexSimpleApi
635
+
636
+ async def main():
637
+ async with AsWebexSimpleApi(concurrent_requests=40) as api:
638
+ calling_users = [u for u in await api.people.list(calling_data=True)
639
+ if u.location_id]
640
+
641
+ # Get voicemail settings for all users in parallel
642
+ vm_settings = await asyncio.gather(
643
+ *[api.person_settings.voicemail.read(person_id=u.person_id)
644
+ for u in calling_users]
645
+ )
646
+ for user, vm in zip(calling_users, vm_settings):
647
+ print(f'{user.display_name}: rings={vm.send_unanswered_calls.number_of_rings}')
648
+
649
+ asyncio.run(main())
650
+ ```
651
+
652
+ ### 5.5 Bulk Update Person Settings (Sync with ThreadPoolExecutor)
653
+
654
+ ```python
655
+ from concurrent.futures import ThreadPoolExecutor
656
+ from wxc_sdk import WebexSimpleApi
657
+
658
+ api = WebexSimpleApi()
659
+ calling_users = [u for u in api.people.list(calling_data=True) if u.location_id]
660
+
661
+ def set_rings(user):
662
+ vm = api.person_settings.voicemail
663
+ settings = vm.read(person_id=user.person_id)
664
+ settings.send_unanswered_calls.number_of_rings = 6
665
+ vm.configure(user.person_id, settings=settings)
666
+
667
+ with ThreadPoolExecutor() as pool:
668
+ list(pool.map(set_rings, calling_users))
669
+ ```
670
+
671
+ ### 5.6 Reset Call Forwarding for All Users (Async)
672
+
673
+ ```python
674
+ import asyncio
675
+ from wxc_sdk.all_types import PersonForwardingSetting
676
+ from wxc_sdk.as_api import AsWebexSimpleApi
677
+
678
+ async def main():
679
+ async with AsWebexSimpleApi() as api:
680
+ calling_users = [u for u in await api.people.list(calling_data=True)
681
+ if u.location_id]
682
+ forwarding = PersonForwardingSetting.default()
683
+ await asyncio.gather(
684
+ *[api.person_settings.forwarding.configure(
685
+ entity_id=u.person_id, forwarding=forwarding)
686
+ for u in calling_users]
687
+ )
688
+ print(f'Reset forwarding for {len(calling_users)} users')
689
+
690
+ asyncio.run(main())
691
+ ```
692
+
693
+ ### 5.7 Call Queue Management
694
+
695
+ **List all queues:**
696
+ ```python
697
+ queues = list(api.telephony.callqueue.list())
698
+ ```
699
+
700
+ **Get queue details (includes agents):**
701
+ ```python
702
+ details = api.telephony.callqueue.details(
703
+ location_id=queue.location_id,
704
+ queue_id=queue.id
705
+ )
706
+ for agent in details.agents:
707
+ print(f' {agent.first_name} {agent.last_name}: '
708
+ f'{"joined" if agent.join_enabled else "not joined"}')
709
+ ```
710
+
711
+ **Update queue agents:**
712
+ ```python
713
+ from wxc_sdk.telephony.callqueue import CallQueue
714
+ from wxc_sdk.telephony.hg_and_cq import Agent
715
+
716
+ # Add an agent
717
+ details.agents.append(Agent(agent_id=user.person_id))
718
+ update = CallQueue(agents=details.agents)
719
+ api.telephony.callqueue.update(
720
+ location_id=queue.location_id,
721
+ queue_id=queue.id,
722
+ update=update,
723
+ )
724
+ ```
725
+
726
+ **Important:** The `list()` response for queues does NOT include agents. You must call `details()` to get the full agent list.
727
+
728
+ ### 5.8 Holiday Schedule Provisioning
729
+
730
+ ```python
731
+ from wxc_sdk.all_types import Event, Schedule, ScheduleType
732
+
733
+ # Create a holiday schedule
734
+ schedule = Schedule(
735
+ name='National Holidays',
736
+ schedule_type=ScheduleType.holidays,
737
+ events=[
738
+ Event(
739
+ name='Independence Day 2026',
740
+ start_date=date(2026, 7, 4),
741
+ end_date=date(2026, 7, 4),
742
+ all_day_enabled=True,
743
+ ),
744
+ ],
745
+ )
746
+ schedule_id = api.telephony.schedules.create(
747
+ obj_id=location.location_id,
748
+ schedule=schedule,
749
+ )
750
+ ```
751
+
752
+ ### 5.9 List Phone Numbers
753
+
754
+ ```python
755
+ # All phone numbers in the org
756
+ all_numbers = api.telephony.phone_numbers()
757
+
758
+ # Available (unassigned) numbers in a location
759
+ available = api.telephony.phone_numbers(
760
+ available=True,
761
+ location_id=location.location_id,
762
+ )
763
+
764
+ # Numbers owned by workspaces (places)
765
+ from wxc_sdk.common import OwnerType
766
+ place_numbers = api.telephony.phone_numbers(
767
+ owner_type=OwnerType.place,
768
+ )
769
+ ```
770
+
771
+ ### 5.10 Hunt Group with Call Forwarding
772
+
773
+ ```python
774
+ from wxc_sdk.telephony.huntgroup import HuntGroup
775
+
776
+ # Create a hunt group
777
+ settings = HuntGroup.create(
778
+ name='Support HG',
779
+ phone_number='+14085551234',
780
+ )
781
+ hg_id = api.telephony.huntgroup.create(
782
+ location_id=location.location_id,
783
+ settings=settings,
784
+ )
785
+
786
+ # Set call forwarding on the hunt group
787
+ forwarding = api.telephony.huntgroup.forwarding.settings(
788
+ location_id=location.location_id,
789
+ feature_id=hg_id,
790
+ )
791
+ forwarding.always.enabled = True
792
+ forwarding.always.destination = '+14085559999'
793
+ api.telephony.huntgroup.forwarding.update(
794
+ location_id=location.location_id,
795
+ feature_id=hg_id,
796
+ forwarding=forwarding,
797
+ )
798
+ ```
799
+
800
+ ### 5.11 Users Without Devices
801
+
802
+ ```python
803
+ import asyncio
804
+ from wxc_sdk.as_api import AsWebexSimpleApi
805
+
806
+ async def main():
807
+ async with AsWebexSimpleApi(tokens=tokens) as api:
808
+ calling_users = [u for u in await api.people.list(calling_data=True)
809
+ if u.location_id]
810
+ device_infos = await asyncio.gather(
811
+ *[api.person_settings.devices(person_id=u.person_id)
812
+ for u in calling_users]
813
+ )
814
+ users_wo_devices = [
815
+ u for u, di in zip(calling_users, device_infos)
816
+ if not di.devices
817
+ ]
818
+ print(f'{len(users_wo_devices)} users without devices')
819
+
820
+ asyncio.run(main())
821
+ ```
822
+
823
+ ### 5.12 Workspace Provisioning with 3rd-Party Device
824
+
825
+ ```python
826
+ from wxc_sdk.workspaces import (
827
+ CallingType, Workspace, WorkspaceCalling,
828
+ WorkspaceSupportedDevices, WorkspaceWebexCalling,
829
+ )
830
+ from wxc_sdk.common import DevicePlatform
831
+
832
+ # Create workspace
833
+ settings = Workspace(
834
+ location_id=location.location_id,
835
+ display_name='Lobby Phone',
836
+ capacity=1,
837
+ supported_devices=WorkspaceSupportedDevices.phones,
838
+ device_platform=DevicePlatform.cisco,
839
+ calling=WorkspaceCalling(
840
+ type=CallingType.webex,
841
+ webex_calling=WorkspaceWebexCalling(
842
+ licenses=[calling_license_id],
843
+ extension='2001',
844
+ location_id=location.location_id,
845
+ ),
846
+ ),
847
+ )
848
+ workspace = await api.workspaces.create(settings=settings)
849
+
850
+ # Attach a device by MAC address
851
+ device = await api.devices.create_by_mac_address(
852
+ mac='DEADDEAD0001',
853
+ workspace_id=workspace.workspace_id,
854
+ model='Generic IPPhone Customer Managed',
855
+ password='generated_password',
856
+ )
857
+ ```
858
+
859
+ ---
860
+
861
+ ## 6. Error Handling
862
+
863
+ ### RestError (Sync)
864
+
865
+ The sync API raises `wxc_sdk.rest.RestError` (subclass of `requests.HTTPError`) on API errors:
866
+
867
+ ```python
868
+ from wxc_sdk.rest import RestError
869
+
870
+ try:
871
+ api.telephony.callqueue.details(
872
+ location_id='bad-id',
873
+ queue_id='bad-id',
874
+ )
875
+ except RestError as e:
876
+ print(f'HTTP {e.response.status_code}')
877
+ print(f'Error code: {e.code}')
878
+ print(f'Description: {e.description}')
879
+ print(f'Tracking ID: {e.detail.tracking_id}')
880
+ ```
881
+
882
+ `RestError.detail` is an `ErrorDetail` object with:
883
+ - `message` -- error message
884
+ - `errors` -- list of `SingleError` (each has `description` and `error_code`)
885
+ - `tracking_id` -- Webex tracking ID for support escalation
886
+
887
+ ### AsRestError (Async)
888
+
889
+ The async API raises `wxc_sdk.as_rest.AsRestError` (subclass of `aiohttp.ClientResponseError`):
890
+
891
+ ```python
892
+ from wxc_sdk.as_rest import AsRestError
893
+
894
+ try:
895
+ await api.telephony.callqueue.details(
896
+ location_id='bad-id',
897
+ queue_id='bad-id',
898
+ )
899
+ except AsRestError as e:
900
+ print(f'HTTP {e.status}')
901
+ print(f'Detail: {e.detail}')
902
+ ```
903
+
904
+ ### Handling Errors in Bulk Operations
905
+
906
+ Use `return_exceptions=True` with `asyncio.gather` to collect errors without stopping:
907
+
908
+ ```python
909
+ results = await asyncio.gather(
910
+ *[do_something(item) for item in items],
911
+ return_exceptions=True,
912
+ )
913
+ for item, result in zip(items, results):
914
+ if isinstance(result, Exception):
915
+ print(f'Failed on {item}: {result}')
916
+ ```
917
+
918
+ ---
919
+
920
+ ## 7. REST Client Details
921
+
922
+ ### Base URL
923
+
924
+ All API requests go to `https://webexapis.com/v1`.
925
+
926
+ ### Request Headers (Auto-Set)
927
+
928
+ Every request from `RestSession` / `AsRestSession` includes:
929
+
930
+ ```
931
+ Authorization: Bearer {access_token}
932
+ Content-Type: application/json;charset=utf-8
933
+ TrackingID: SIMPLE_{uuid4}
934
+ ```
935
+
936
+ ### Rate Limiting & 429 Retry
937
+
938
+ When `retry_429=True` (the default), the SDK automatically:
939
+
940
+ 1. Catches HTTP 429 responses
941
+ 2. Reads the `Retry-After` header (defaults to 5 seconds if missing)
942
+ 3. Caps the wait at `RETRY_429_MAX_WAIT` (60 seconds, defined in `wxc_sdk.base`)
943
+ 4. Sleeps for the specified duration, then retries
944
+
945
+ The retry loop runs inside a Semaphore-protected block, so the concurrency limit is respected.
946
+
947
+ **Sync:** Uses `time.sleep(retry_after)`
948
+ **Async:** Uses `await asyncio.sleep(retry_after)`
949
+
950
+ ### Concurrency Control
951
+
952
+ Both sync and async sessions use a Semaphore to limit concurrent requests:
953
+
954
+ ```python
955
+ # Sync: threading.Semaphore
956
+ # Async: asyncio.Semaphore
957
+ self._sem = Semaphore(concurrent_requests)
958
+ ```
959
+
960
+ Every request acquires the semaphore before execution. This prevents overwhelming the Webex API. The `concurrent_requests` parameter controls the limit (default: 10).
961
+
962
+ For bulk async operations, you can raise this significantly:
963
+
964
+ ```python
965
+ async with AsWebexSimpleApi(concurrent_requests=40) as api:
966
+ ...
967
+
968
+ # Some examples go as high as 100:
969
+ async with AsWebexSimpleApi(tokens=tokens, concurrent_requests=100) as api:
970
+ ...
971
+ ```
972
+
973
+ ### Pagination (Automatic)
974
+
975
+ The SDK handles RFC 5988 pagination transparently via `follow_pagination()`. List methods return generators (sync) or async generators that automatically fetch subsequent pages by following the `next` link header.
976
+
977
+ You never need to handle pagination manually. Just iterate:
978
+
979
+ ```python
980
+ # This fetches ALL pages automatically
981
+ all_users = list(api.people.list(calling_data=True))
982
+ ```
983
+
984
+ Internally, `follow_pagination()`:
985
+ 1. Makes the initial GET request
986
+ 2. Parses `response.links['next']['url']` for the next page
987
+ 3. Yields each item from the `items` key (or the first list-valued key in the response)
988
+ 4. Repeats until no `next` link is present
989
+
990
+ ### Proxy and SSL Support
991
+
992
+ ```python
993
+ # Sync
994
+ api = WebexSimpleApi(
995
+ tokens=tokens,
996
+ proxy_url='https://proxy.corp.com:8080',
997
+ verify='/path/to/ca-bundle.crt', # or False to disable
998
+ )
999
+
1000
+ # Async
1001
+ api = AsWebexSimpleApi(
1002
+ tokens=tokens,
1003
+ proxy_url='https://proxy.corp.com:8080',
1004
+ ssl=ssl_context, # ssl.SSLContext or bool
1005
+ )
1006
+ ```
1007
+
1008
+ ### Response Callbacks
1009
+
1010
+ You can register callbacks to inspect every HTTP response (useful for logging, HAR capture, metrics):
1011
+
1012
+ ```python
1013
+ def my_callback(response, diff_ns):
1014
+ print(f'{response.request.method} {response.request.url} '
1015
+ f'took {diff_ns / 1e6:.1f}ms')
1016
+
1017
+ callback_id = api.session.register_response_callback(my_callback)
1018
+ # ... do work ...
1019
+ api.session.unregister_response_callback(callback_id)
1020
+ ```
1021
+
1022
+ The SDK automatically registers a debug-level logging callback that dumps full request/response details when `logging.DEBUG` is enabled.
1023
+
1024
+ ### HAR Writer
1025
+
1026
+ Several examples use `wxc_sdk.har_writer.HarWriter` for saving HTTP traces in HAR format:
1027
+
1028
+ ```python
1029
+ from wxc_sdk.har_writer import HarWriter
1030
+
1031
+ async with AsWebexSimpleApi(tokens=tokens) as api:
1032
+ with HarWriter(api=api, path='trace.har'):
1033
+ # all requests during this block are captured
1034
+ await api.people.list()
1035
+ ```
1036
+
1037
+ ---
1038
+
1039
+ ## 8. Data Types
1040
+
1041
+ ### Import Patterns
1042
+
1043
+ The SDK provides a convenience module `wxc_sdk.all_types` that re-exports all model types:
1044
+
1045
+ ```python
1046
+ # Import everything (convenient for interactive/scripts)
1047
+ from wxc_sdk.all_types import *
1048
+
1049
+ # Import specific types (preferred for production code)
1050
+ from wxc_sdk.all_types import PersonForwardingSetting, Schedule, ScheduleType, Event
1051
+ ```
1052
+
1053
+ For types not in `all_types`, import from the specific module:
1054
+
1055
+ ```python
1056
+ from wxc_sdk.people import Person
1057
+ from wxc_sdk.telephony.callqueue import CallQueue
1058
+ from wxc_sdk.telephony.hg_and_cq import Agent
1059
+ from wxc_sdk.telephony.huntgroup import HuntGroup
1060
+ from wxc_sdk.telephony.forwarding import CallForwarding
1061
+ from wxc_sdk.telephony import NumberListPhoneNumber
1062
+ from wxc_sdk.telephony.location import TelephonyLocation
1063
+ from wxc_sdk.telephony.devices import DeviceMember, DeviceMembersResponse
1064
+ from wxc_sdk.common import UserType, OwnerType, AlternateNumber, RingPattern, IdAndName
1065
+ from wxc_sdk.locations import Location
1066
+ from wxc_sdk.licenses import License
1067
+ from wxc_sdk.workspaces import Workspace, CallingType, WorkspaceCalling
1068
+ from wxc_sdk.person_settings import TelephonyDevice, DeviceList
1069
+ from wxc_sdk.person_settings.permissions_out import Action, DigitPattern
1070
+ from wxc_sdk.authorizations import Authorization, AuthorizationType
1071
+ from wxc_sdk.tokens import Tokens
1072
+ from wxc_sdk.integration import Integration
1073
+ from wxc_sdk.scopes import parse_scopes
1074
+ from wxc_sdk.webhook import WebhookEvent, WebhookEventType
1075
+ ```
1076
+
1077
+ ### ApiModel Base Class
1078
+
1079
+ All data models inherit from `wxc_sdk.base.ApiModel` (which extends `pydantic.BaseModel`):
1080
+
1081
+ - **Alias generation:** Python `snake_case` attributes are automatically mapped to `camelCase` JSON keys via `to_camel`.
1082
+ - **Populate by name:** Both `snake_case` and `camelCase` field names are accepted.
1083
+ - **Extra fields allowed:** By default, unknown JSON fields are kept (configurable via `API_MODEL_ALLOW_EXTRA` env var).
1084
+ - **Enum values stored:** Enum fields store the value, not the enum member.
1085
+ - **Serialization defaults:** `model_dump_json()` excludes `None` values and uses aliases by default.
1086
+
1087
+ ### SafeEnum
1088
+
1089
+ `wxc_sdk.base.SafeEnum` extends `aenum.Enum` to automatically handle unknown enum values from the API. If the API returns a value not defined in the enum, it is dynamically added (with a warning log) rather than raising an error. This prevents breakage when Webex adds new enum values.
1090
+
1091
+ ### Utility Functions
1092
+
1093
+ ```python
1094
+ from wxc_sdk.base import webex_id_to_uuid, dt_iso_str, plus1
1095
+
1096
+ # Convert a Webex base64-encoded ID to a UUID
1097
+ uuid = webex_id_to_uuid('Y2lzY29zcGFyazovL3VzL...')
1098
+
1099
+ # Format datetime for Webex API (UTC, milliseconds)
1100
+ from datetime import datetime
1101
+ iso = dt_iso_str(datetime.now()) # '2026-03-16T12:00:00.000Z'
1102
+
1103
+ # Convert 10-digit number to +E.164
1104
+ phone = plus1('4085551234') # '+14085551234'
1105
+ ```
1106
+
1107
+ ---
1108
+
1109
+ ## 9. Environment Variable Patterns
1110
+
1111
+ ### Recommended .env Structure
1112
+
1113
+ Most examples follow this pattern: a `.env` file named after the script.
1114
+
1115
+ ```
1116
+ # For simple scripts: .env
1117
+ WEBEX_ACCESS_TOKEN=NjY2Yz...
1118
+
1119
+ # For OAuth integration: get_tokens.env
1120
+ TOKEN_INTEGRATION_CLIENT_ID=C1234...
1121
+ TOKEN_INTEGRATION_CLIENT_SECRET=abc123...
1122
+ TOKEN_INTEGRATION_CLIENT_SCOPES="spark-admin:telephony_config_read"
1123
+
1124
+ # For service apps: service_app.env
1125
+ SERVICE_APP_CLIENT_ID=C5678...
1126
+ SERVICE_APP_CLIENT_SECRET=xyz789...
1127
+ SERVICE_APP_REFRESH_TOKEN=MmI3Zj...
1128
+ ```
1129
+
1130
+ ### Token Caching Pattern
1131
+
1132
+ All OAuth/service-app examples cache tokens to a YML file and check `tokens.remaining` before use:
1133
+
1134
+ ```python
1135
+ import yaml
1136
+ from wxc_sdk.tokens import Tokens
1137
+
1138
+ def read_tokens(path: str) -> Tokens:
1139
+ with open(path) as f:
1140
+ return Tokens.model_validate(yaml.safe_load(f))
1141
+
1142
+ def write_tokens(path: str, tokens: Tokens):
1143
+ with open(path, mode='w') as f:
1144
+ yaml.safe_dump(tokens.model_dump(exclude_none=True), f)
1145
+
1146
+ # Refresh if less than 24 hours remaining
1147
+ if tokens.remaining < 24 * 60 * 60:
1148
+ tokens = refresh_tokens()
1149
+ ```
1150
+
1151
+ ---
1152
+
1153
+ ## 10. Logging
1154
+
1155
+ The SDK uses Python's standard `logging` module. Key loggers:
1156
+
1157
+ | Logger | Purpose |
1158
+ |--------|---------|
1159
+ | `wxc_sdk.rest` | Sync REST request/response details |
1160
+ | `wxc_sdk.as_rest` | Async REST request/response details |
1161
+ | `wxc_sdk` | General SDK messages |
1162
+
1163
+ ### Enable Full Request Logging
1164
+
1165
+ ```python
1166
+ import logging
1167
+
1168
+ logging.basicConfig(level=logging.DEBUG)
1169
+ # Optional: reduce noise from urllib3
1170
+ logging.getLogger('urllib3').setLevel(logging.INFO)
1171
+ ```
1172
+
1173
+ ### Log REST to File Only
1174
+
1175
+ ```python
1176
+ import logging
1177
+
1178
+ # Console: INFO only
1179
+ logging.basicConfig(level=logging.INFO)
1180
+
1181
+ # REST details to file at DEBUG
1182
+ rest_logger = logging.getLogger('wxc_sdk.as_rest')
1183
+ rest_logger.setLevel(logging.DEBUG)
1184
+ handler = logging.FileHandler('rest.log', mode='w')
1185
+ handler.setLevel(logging.DEBUG)
1186
+ rest_logger.addHandler(handler)
1187
+ ```
1188
+
1189
+ The SDK's response dump (enabled at DEBUG level) includes:
1190
+ - Request method, URL, status code, timing
1191
+ - Request headers (Authorization masked as `Bearer ***`)
1192
+ - Request body (form data secrets masked)
1193
+ - Response headers and body
1194
+ - Access/refresh tokens in response bodies are masked as `***`
1195
+
1196
+ ---
1197
+
1198
+ ## 11. Key Gotchas
1199
+
1200
+ 1. **`calling_data=True` is required** -- `api.people.list()` without `calling_data=True` will not populate `location_id`, phone numbers, or other calling-specific fields.
1201
+
1202
+ 2. **`list()` returns minimal data** -- Queue, hunt group, and auto attendant list responses omit details like agents and alternate numbers. Always call `details()` to get the full object.
1203
+
1204
+ 3. **`concurrent_requests` default is 10** -- This is conservative. For bulk async operations, increase to 40-100.
1205
+
1206
+ 4. **Async list methods may differ** -- Some APIs have both `.list()` (returns a full list) and `.list_gen()` (returns an async generator). Check which is available for your use case.
1207
+
1208
+ 5. **`MAX_USERS_WITH_CALLING_DATA = 10`** -- The SDK internally limits concurrent requests for users with calling data to prevent API issues. This is hardcoded in `as_api.py`.
1209
+
1210
+ 6. **Thread safety (sync)** -- The sync `RestSession` uses a `threading.Semaphore` so it is safe to share across threads. Use `ThreadPoolExecutor` for parallel work with the sync API.
1211
+
1212
+ 7. **Service app tokens expire** -- Always check `tokens.remaining` and refresh proactively. The examples use a 24-hour threshold.
1213
+
1214
+ 8. **Webex IDs are base64-encoded** -- Use `webex_id_to_uuid()` if you need the raw UUID (e.g., for matching against other systems).
1215
+
1216
+ 9. **Agent model imports differ per feature** -- Hunt Groups and Call Queues use `from wxc_sdk.telephony.hg_and_cq import Agent`. Call Park and Call Pickup use `from wxc_sdk.common import PersonPlaceAgent`. Paging uses `from wxc_sdk.telephony.paging import PagingAgent`. Do NOT try importing Agent from the individual feature modules.
1217
+
1218
+ 10. **Named delete methods vs generic `.delete()`** -- Call Park, Call Pickup, Paging, and several other APIs inherit a generic `.delete()` that accepts anything silently. Always use the named method: `delete_callpark()`, `delete_pickup()`, `delete_paging()`, `delete_huntgroup()`, `delete_queue()`, `delete_auto_attendant()`, `delete_schedule()`.
1219
+
1220
+ 11. **`ScheduleApi` import path** -- Lives at `wxc_sdk.common.schedules`, NOT `wxc_sdk.telephony.schedules` (which doesn't exist). `from wxc_sdk.common.schedules import ScheduleApi, Schedule, Event, ScheduleType`.
1221
+
1222
+ 12. **`PagingApi.update()` unusual parameter order** -- Signature is `(location_id, update: Paging, paging_id)` — the model object comes BEFORE the ID. Always use keyword arguments to avoid silent misassignment.
1223
+
1224
+ ---
1225
+
1226
+ ## See Also
1227
+
1228
+ - **`authentication.md`** — Detailed OAuth flows, scope reference, and common auth error handling.
1229
+ - **`provisioning.md`** — End-to-end user provisioning workflows, license assignment (People API and PATCH methods), and location management.