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.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- wxcli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
<!-- Not directly verified via CLI — documents wxcadm library (not generated CLI commands) -->
|
|
2
|
+
<!-- wxcadm v4.6.1 review 2026-03-27: All v4.5.0–v4.6.1 changes verified present. XSI(org, person) constructor signature, RealtimeClass(api, data_url, api_fields) signature, org.api.headers usage for XSI auth — all accurately documented. No discrepancies found. -->
|
|
3
|
+
# wxcadm XSI & Real-time Call Monitoring
|
|
4
|
+
|
|
5
|
+
## Why This Matters
|
|
6
|
+
|
|
7
|
+
XSI (eXtended Services Interface) is the one thing wxcadm can do that wxc_sdk cannot. While wxc_sdk covers the public Webex REST APIs for provisioning, configuration, and admin tasks, it has no access to the real-time call control plane. XSI connects directly to the BroadWorks call control back-end that powers Webex Calling, giving you:
|
|
8
|
+
|
|
9
|
+
- **Real-time event streams** — every call placed, received, held, transferred, parked, and released fires an event you can consume
|
|
10
|
+
- **Programmatic call control** — originate, hold, resume, transfer, conference, park, record, and hang up calls from code
|
|
11
|
+
- **Live call state** — query active calls, get call status, see remote party info and endpoint details
|
|
12
|
+
- **Directory access** — search Enterprise, Group, and Personal directories directly from the call control platform
|
|
13
|
+
- **Service configuration reads** — pull BLF/monitoring, Executive/Assistant, SNR, anonymous call rejection settings live from the platform (not cached admin data)
|
|
14
|
+
|
|
15
|
+
None of this is available through wxc_sdk or the standard Webex REST APIs.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<!-- Updated by playbook session 2026-03-18 -->
|
|
20
|
+
## When to Use wxcadm vs Raw HTTP
|
|
21
|
+
|
|
22
|
+
**XSI is wxcadm's killer feature. There is no raw HTTP alternative for any of this.**
|
|
23
|
+
|
|
24
|
+
| Use wxcadm when | Use raw HTTP when |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Real-time call event streaming (XSI-Events) | **Never** -- XSI-Events requires persistent HTTP streaming with heartbeats, auto-refresh, and DNS SRV discovery that wxcadm handles entirely |
|
|
27
|
+
| Programmatic call control (originate, hold, transfer, park, record) | **Never** -- XSI-Actions endpoints are on the BroadWorks XSP, not the standard Webex REST API |
|
|
28
|
+
| Live call state queries, directory search, service config reads | **Never** -- these hit BroadWorks directly via XSI, not available through `api.webex.com` |
|
|
29
|
+
| CRM screen pops, real-time dashboards, call center queue routing | **Never** -- all require XSI event streaming |
|
|
30
|
+
|
|
31
|
+
> **Note:** The playbook uses raw HTTP via `api.session.rest_*()` for standard CRUD operations against `api.webex.com`. XSI operates on an entirely different infrastructure (BroadWorks XSP) and **cannot** be replaced by raw HTTP calls to the Webex REST API. If you need real-time call monitoring or programmatic call control, wxcadm is the only option.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## XSI Architecture Overview
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Your App (wxcadm)
|
|
39
|
+
|
|
|
40
|
+
|--- XSI-Actions (REST calls → call control, profile, services, directory)
|
|
41
|
+
|--- XSI-Events (long-running HTTP streaming → real-time event delivery)
|
|
42
|
+
|
|
|
43
|
+
v
|
|
44
|
+
Webex Calling XSP (eXtended Services Platform)
|
|
45
|
+
|
|
|
46
|
+
v
|
|
47
|
+
BroadWorks Call Control Back-end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
XSI exposes two distinct interfaces:
|
|
51
|
+
|
|
52
|
+
| Interface | Transport | Purpose |
|
|
53
|
+
|-----------|-----------|---------|
|
|
54
|
+
| **XSI-Actions** | Standard REST (JSON/XML) | Call control commands, profile/service reads, directory search |
|
|
55
|
+
| **XSI-Events** | Long-running HTTP stream | Asynchronous event delivery for call state changes |
|
|
56
|
+
|
|
57
|
+
The XSI endpoints are unique per Org. wxcadm discovers them via the `v1/organizations/{id}?callingData=true` API call, which returns:
|
|
58
|
+
- `xsiActionsEndpoint` — base URL for call control and data queries
|
|
59
|
+
- `xsiEventsEndpoint` — base URL for event subscription management
|
|
60
|
+
- `xsiEventsChannelEndpoint` — base URL for opening streaming channels
|
|
61
|
+
- `xsiDomain` — domain used for SRV record lookups to find all XSPs
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Setup & Authentication
|
|
66
|
+
|
|
67
|
+
### Prerequisites
|
|
68
|
+
|
|
69
|
+
1. **XSI must be enabled on your Webex Org.** This is not on by default. Contact Cisco TAC to request activation.
|
|
70
|
+
2. **API token scopes**: Your Integration or access token must include:
|
|
71
|
+
- `spark-admin:xsi` — admin-level XSI access
|
|
72
|
+
- `spark:xsi` — user-level XSI access (if enabled)
|
|
73
|
+
- The 12-hour developer token from developer.webex.com includes all necessary scopes automatically.
|
|
74
|
+
|
|
75
|
+
### Initializing XSI
|
|
76
|
+
|
|
77
|
+
Two approaches to get XSI endpoints:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import wxcadm
|
|
81
|
+
|
|
82
|
+
access_token = "Your API Access Token"
|
|
83
|
+
|
|
84
|
+
# Option 1: Get XSI endpoints at Webex init time
|
|
85
|
+
webex = wxcadm.Webex(access_token, get_xsi=True)
|
|
86
|
+
|
|
87
|
+
# Option 2: Get XSI endpoints later
|
|
88
|
+
webex = wxcadm.Webex(access_token)
|
|
89
|
+
webex.org.get_xsi_endpoints()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`get_xsi_endpoints()` calls the Org API with `callingData=true` and populates `webex.org.xsi` with the four endpoint URLs. If XSI is not enabled, it returns `None`.
|
|
93
|
+
|
|
94
|
+
### Starting an XSI Session for a Person
|
|
95
|
+
|
|
96
|
+
XSI operates per-user. You must start an XSI session for each Person you want to control or query:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
person = webex.org.get_person_by_email("user@domain.com")
|
|
100
|
+
person.start_xsi() # Creates person.xsi (an XSI instance)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Signature:**
|
|
104
|
+
```python
|
|
105
|
+
Person.start_xsi(get_profile: bool = False, cache: bool = False) -> XSI
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `get_profile=True` — automatically fetches the XSI profile (phone number, extension, etc.) during init
|
|
109
|
+
- `cache=True` — caches XSI data so subsequent property accesses don't re-query the platform. Default is live (uncached).
|
|
110
|
+
|
|
111
|
+
The `start_xsi()` method creates an `XSI` instance and stores it at `person.xsi`. The XSI instance sets up:
|
|
112
|
+
- HTTP headers with `X-BroadWorks-Protocol-Version: 25.0`, JSON accept/content-type, plus the Webex auth token
|
|
113
|
+
- Query params with `format=json`
|
|
114
|
+
- A user ID derived from the Person's Spark ID (the BroadWorks user identifier)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## XSI Profile & Services (XSI-Actions)
|
|
119
|
+
|
|
120
|
+
Once `person.start_xsi()` has been called, you can read data directly from the call control platform.
|
|
121
|
+
|
|
122
|
+
### Profile
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
person.start_xsi(get_profile=True)
|
|
126
|
+
print(person.xsi.profile)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Returns a dict with:
|
|
130
|
+
|
|
131
|
+
| Key | Description |
|
|
132
|
+
|-----|-------------|
|
|
133
|
+
| `country_code` | Country dialing code |
|
|
134
|
+
| `number` | Phone number (if assigned) |
|
|
135
|
+
| `extension` | Extension (if assigned) |
|
|
136
|
+
| `user_id` | BroadWorks user ID |
|
|
137
|
+
| `group_id` | BroadWorks group ID (maps to Webex Calling Location) |
|
|
138
|
+
| `service_provider` | Service provider identifier |
|
|
139
|
+
| `registrations_url` | URL to query device registrations |
|
|
140
|
+
| `schedule_url` | URL to query user schedules |
|
|
141
|
+
| `fac_url` | URL to query Feature Access Codes |
|
|
142
|
+
| `raw` | The complete raw profile response |
|
|
143
|
+
|
|
144
|
+
### Device Registrations
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
registrations = person.xsi.registrations
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Returns the device registrations associated with the person (phones, Webex app, etc.). Requires the profile to have been fetched first (auto-fetches if needed).
|
|
151
|
+
|
|
152
|
+
### Services
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
services = person.xsi.get_services()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Queries all assigned services and their configurations. Returns a dict keyed by service name with the full service config as the value. Services without configuration data are stored as `True`.
|
|
159
|
+
|
|
160
|
+
### Individual Service Properties
|
|
161
|
+
|
|
162
|
+
These are available as properties with lazy loading (fetched on first access, or every access if `cache=False`):
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
person.xsi.executive # Executive settings
|
|
166
|
+
person.xsi.executive_assistant # Executive Assistant settings
|
|
167
|
+
person.xsi.monitoring # BLF/Busy Lamp Field settings
|
|
168
|
+
person.xsi.single_number_reach # SNR / Office Anywhere settings
|
|
169
|
+
person.xsi.anonymous_call_rejection # Anonymous Call Rejection settings
|
|
170
|
+
person.xsi.alternate_numbers # Alternate Numbers
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Each property queries: `GET /v2.0/user/{userId}/services/{ServiceName}`
|
|
174
|
+
|
|
175
|
+
### Feature Access Codes
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
fac_list = person.xsi.fac_list
|
|
179
|
+
for fac in fac_list:
|
|
180
|
+
print(f"{fac.feature}: {fac.code} (alt: {fac.alternate_code})")
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Returns a list of `FeatureAccessCode` objects with `.feature`, `.code`, and `.alternate_code` attributes.
|
|
184
|
+
|
|
185
|
+
### Directory Search
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
results = person.xsi.directory(
|
|
189
|
+
type='Enterprise', # or 'Group', 'Personal'
|
|
190
|
+
first_name='John',
|
|
191
|
+
last_name='Smith',
|
|
192
|
+
any_match=False # True = OR logic across filters
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Signature:**
|
|
197
|
+
```python
|
|
198
|
+
XSI.directory(
|
|
199
|
+
type: str = 'Enterprise',
|
|
200
|
+
first_name: str = None,
|
|
201
|
+
last_name: str = None,
|
|
202
|
+
name: str = None,
|
|
203
|
+
user_id: str = None,
|
|
204
|
+
group_id: str = None,
|
|
205
|
+
number: str = None,
|
|
206
|
+
extension: str = None,
|
|
207
|
+
mobile_number: str = None,
|
|
208
|
+
department: str = None,
|
|
209
|
+
email: str = None,
|
|
210
|
+
any_match: bool = False
|
|
211
|
+
) -> list[dict]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Filter availability by directory type:**
|
|
215
|
+
|
|
216
|
+
| Directory Type | Available Filters |
|
|
217
|
+
|----------------|-------------------|
|
|
218
|
+
| Enterprise | first_name, last_name, name, user_id, group_id, number, extension, mobile_number, department, email |
|
|
219
|
+
| Group | first_name, last_name, name, user_id, group_id, number, extension, mobile_number, department, email |
|
|
220
|
+
| Personal | name, number |
|
|
221
|
+
|
|
222
|
+
**Case-insensitive search:** Append `/i` to the search string, e.g. `first_name='John/i'` matches "John", "john", "JOHN".
|
|
223
|
+
|
|
224
|
+
Handles pagination automatically (fetches 50 records at a time until all results are retrieved).
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Call Control via XSI (XSI-Actions)
|
|
229
|
+
|
|
230
|
+
### Creating and Originating Calls
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
person.start_xsi()
|
|
234
|
+
|
|
235
|
+
# Create a call object, then originate
|
|
236
|
+
call = person.xsi.new_call()
|
|
237
|
+
call.originate("17192662837")
|
|
238
|
+
|
|
239
|
+
# Or create and originate in one step
|
|
240
|
+
call = person.xsi.new_call(address="17192662837")
|
|
241
|
+
|
|
242
|
+
# One-liner for simple click-to-dial
|
|
243
|
+
person.start_xsi().new_call().originate("17192662837")
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**`XSI.new_call()` signature:**
|
|
247
|
+
```python
|
|
248
|
+
XSI.new_call(
|
|
249
|
+
address: str = None, # Phone number to dial
|
|
250
|
+
phone: str = 'All', # 'All', 'Primary', or 'SharedCallAppearance'
|
|
251
|
+
aor: str = None # Address-of-record for shared devices
|
|
252
|
+
) -> Call
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**`Call.originate()` signature:**
|
|
256
|
+
```python
|
|
257
|
+
Call.originate(
|
|
258
|
+
address: str, # Phone number or extension to call
|
|
259
|
+
comment: str = "", # Text comment attached to the call
|
|
260
|
+
phone: str = 'All', # Device selection
|
|
261
|
+
aor: str = None, # Address-of-record for shared devices
|
|
262
|
+
executive: str = None # Extension/number of executive (for assistant calls)
|
|
263
|
+
) -> bool
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
On success, the `Call` object gets populated with:
|
|
267
|
+
- `call.id` — the XSI call ID
|
|
268
|
+
- `call._external_tracking_id` — XSI external tracking ID
|
|
269
|
+
|
|
270
|
+
### Querying Active Calls
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
active_calls = person.xsi.calls # Returns list[Call]
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The `calls` property queries `GET /v2.0/user/{userId}/calls` and creates fresh `Call` instances for each active call. Previous instances are destroyed on each access.
|
|
277
|
+
|
|
278
|
+
### Call Status
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
status = call.status
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Returns a dict:
|
|
285
|
+
```python
|
|
286
|
+
{
|
|
287
|
+
'network_call_id': str, # Unique network-side call ID
|
|
288
|
+
'personality': str, # 'Originator' or 'Terminator'
|
|
289
|
+
'state': str, # 'Active', 'Held', 'Alerting', etc.
|
|
290
|
+
'remote_party': {
|
|
291
|
+
'address': str, # Remote party's address
|
|
292
|
+
'call_type': str # Call type
|
|
293
|
+
},
|
|
294
|
+
'endpoint': {
|
|
295
|
+
'type': str, # Endpoint type
|
|
296
|
+
'aor': str # Address of Record
|
|
297
|
+
},
|
|
298
|
+
'appearance': str, # Call Appearance number
|
|
299
|
+
'diversion_inhibited': bool,
|
|
300
|
+
'start_time': str, # UNIX timestamp
|
|
301
|
+
'answer_time': str, # UNIX timestamp (None if unanswered)
|
|
302
|
+
'status_time': int # UNIX timestamp of this status query
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Hold and Resume
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
call.hold() # Places the call on hold; sets call.held = True
|
|
310
|
+
call.resume() # Takes the call off hold; sets call.held = False
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Both return an `XSIResponse` object (truthy on success, falsy on failure).
|
|
314
|
+
|
|
315
|
+
### Answer an Incoming Call
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
# Answer the most recent incoming call
|
|
319
|
+
answered_call = person.xsi.answer_call()
|
|
320
|
+
|
|
321
|
+
# Or answer a specific call
|
|
322
|
+
answered_call = person.xsi.answer_call(call_id="the-call-id")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Hang Up
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
call.hangup() # Normal hangup
|
|
329
|
+
call.hangup(decline=True) # Decline/reject the call
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Transfer
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
# Blind transfer
|
|
336
|
+
call.transfer("2345")
|
|
337
|
+
|
|
338
|
+
# VM transfer (sends directly to target's voicemail)
|
|
339
|
+
call.transfer("2345", type="vm")
|
|
340
|
+
|
|
341
|
+
# Mute transfer
|
|
342
|
+
call.transfer("2345", type="mute")
|
|
343
|
+
|
|
344
|
+
# Attended transfer (puts current call on hold, dials target)
|
|
345
|
+
call.transfer("2345", type="attended")
|
|
346
|
+
# ... agent and target talk ...
|
|
347
|
+
call.finish_transfer() # Completes the transfer
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**For Call Queue calls:** the address parameter is sent as `phoneno` instead of `address` (handled automatically).
|
|
351
|
+
|
|
352
|
+
### Conference
|
|
353
|
+
|
|
354
|
+
```python
|
|
355
|
+
# Conference from an attended transfer (all parties joined)
|
|
356
|
+
call.transfer("2345", type="attended")
|
|
357
|
+
# ... talk to the target ...
|
|
358
|
+
call.conference() # Bridges all parties
|
|
359
|
+
call.finish_transfer() # Drop the initiator, leave others connected
|
|
360
|
+
|
|
361
|
+
# Conference with a new party (holds current call, dials, bridges on answer)
|
|
362
|
+
conference = call.conference(address="5678")
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**`Conference` class methods:**
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
conference.mute(call_id) # Mute a participant (they can still hear)
|
|
369
|
+
conference.deaf(call_id) # Deaf a participant (they can still be heard)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Call Park
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
# Group Call Park (auto-assigned extension; user must be in a Park Group)
|
|
376
|
+
parked_extension = call.park()
|
|
377
|
+
|
|
378
|
+
# Park at a specific Call Park Extension
|
|
379
|
+
parked_extension = call.park(extension="8001")
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Returns the extension where the call was parked. Raises `NotAllowed` if the user isn't in a Park Group or the extension is busy.
|
|
383
|
+
|
|
384
|
+
### Call Recording Control
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
call.recording("start") # Start recording
|
|
388
|
+
call.recording("stop") # Stop recording (On Demand mode only)
|
|
389
|
+
call.recording("pause") # Pause recording
|
|
390
|
+
call.recording("resume") # Resume paused recording
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Requires Call Recording to be enabled for the user. Raises `NotAllowed` if not enabled.
|
|
394
|
+
|
|
395
|
+
**Known bug in source (verified 2026-03-18):** The `recording()` method has a duplicate `elif action.lower() == "resume"` at line 1528 that was meant to be `"pause"`. The first `"resume"` branch (line 1524) correctly calls `ResumeRecording`. The second (line 1528) maps to `PauseRecording` but is dead code (shadowed by line 1524). This means `recording("pause")` falls through to the `else` branch and raises `ValueError`. **Workaround:** Call the XSI PauseRecording endpoint directly: `requests.put(url + f"/{call_id}/PauseRecording", headers=headers)`.
|
|
396
|
+
|
|
397
|
+
### DTMF
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
call.send_dtmf("12345#") # Send digits
|
|
401
|
+
call.send_dtmf("23456#,123") # Comma = pause between digits
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Executive/Assistant Call Handling
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
# Assistant places a call on behalf of the Executive
|
|
408
|
+
assistant = webex.org.get_person_by_email("assistant@company.com")
|
|
409
|
+
assistant.start_xsi()
|
|
410
|
+
call = assistant.xsi.new_call()
|
|
411
|
+
call.originate("7192662837", executive="1234") # 1234 = exec's extension
|
|
412
|
+
|
|
413
|
+
# Push the call to the Executive's devices
|
|
414
|
+
call.exec_push()
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Reconnect
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
call.reconnect() # Retrieves call from hold AND releases all other calls
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Attach to an Existing Call
|
|
424
|
+
|
|
425
|
+
When you know a call ID (e.g., from an XSI Event), you can create a `Call` object for it:
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
# From an XSI instance
|
|
429
|
+
call = person.xsi.attach_call(call_id="known-call-id")
|
|
430
|
+
|
|
431
|
+
# From an XSICallQueue
|
|
432
|
+
queue = wxcadm.XSICallQueue(call_queue_id="queue-id", org=webex.org)
|
|
433
|
+
call = queue.attach_call(call_id="known-call-id")
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## XSI Response Handling
|
|
439
|
+
|
|
440
|
+
All call control methods (hold, resume, transfer, hangup, etc.) return an `XSIResponse` object:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
response = call.hold()
|
|
444
|
+
if response: # Truthy when HTTP status is OK
|
|
445
|
+
print("Hold successful")
|
|
446
|
+
else:
|
|
447
|
+
print(f"Failed: {response.summary}")
|
|
448
|
+
|
|
449
|
+
response.success # bool
|
|
450
|
+
response.raw_response # dict or text
|
|
451
|
+
response.summary # Error summary text (if error)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## XSI-Events: Real-time Call Monitoring
|
|
457
|
+
|
|
458
|
+
This is the core differentiator. XSI-Events provides a persistent, streaming connection that delivers every call event in the organization (or for a specific user) as it happens.
|
|
459
|
+
|
|
460
|
+
### Architecture
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
XSIEvents (org-level)
|
|
464
|
+
└─ XSIEventsChannelSet (groups channels + manages subscriptions)
|
|
465
|
+
├─ XSIEventsChannel (long-running HTTP stream to XSP #1)
|
|
466
|
+
│ ├─ _channel_daemon thread (reads streaming events)
|
|
467
|
+
│ └─ _heartbeat_daemon thread (sends heartbeats every 15s)
|
|
468
|
+
├─ XSIEventsChannel (long-running HTTP stream to XSP #2)
|
|
469
|
+
│ ├─ _channel_daemon thread
|
|
470
|
+
│ └─ _heartbeat_daemon thread
|
|
471
|
+
└─ XSIEventsSubscription(s) (what events to receive)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Key design decisions in wxcadm:
|
|
475
|
+
- **One channel per XSP**: wxcadm does a DNS SRV lookup on the XSI domain (`_xsi-client._tcp.{domain}`) to discover all XSP servers, then opens a streaming channel to each one
|
|
476
|
+
- **Daemon threads**: Each channel runs two daemon threads — one reading the stream, one sending heartbeats
|
|
477
|
+
- **Python Queue**: All events are delivered into a standard `queue.Queue` you provide, decoupling event production from consumption
|
|
478
|
+
- **Automatic ACKs**: wxcadm automatically acknowledges each event to the server (required by the XSI protocol)
|
|
479
|
+
- **Auto-refresh**: Channels and subscriptions are refreshed every hour (7200-second expiry) to stay alive
|
|
480
|
+
|
|
481
|
+
### Basic Event Monitoring Pattern
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
import queue
|
|
485
|
+
import wxcadm
|
|
486
|
+
|
|
487
|
+
access_token = "Your API Access Token"
|
|
488
|
+
webex = wxcadm.Webex(access_token, get_xsi=True)
|
|
489
|
+
|
|
490
|
+
# Create the XSIEvents instance (org-level)
|
|
491
|
+
events = wxcadm.XSIEvents(webex.org)
|
|
492
|
+
|
|
493
|
+
# Create a queue — set maxsize to prevent unbounded memory growth
|
|
494
|
+
events_queue = queue.Queue(maxsize=500)
|
|
495
|
+
|
|
496
|
+
# Open a channel set (auto-discovers XSPs, creates channels + heartbeat threads)
|
|
497
|
+
channel = events.open_channel(events_queue)
|
|
498
|
+
|
|
499
|
+
# Subscribe to an event package
|
|
500
|
+
subscription = channel.subscribe(["Advanced Call"])
|
|
501
|
+
|
|
502
|
+
# Process events as they arrive
|
|
503
|
+
while True:
|
|
504
|
+
event = events_queue.get()
|
|
505
|
+
event_type = event['xsi:Event']['xsi:eventData']['@xsi1:type']
|
|
506
|
+
print(f"Event: {event_type}")
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Event Packages
|
|
510
|
+
|
|
511
|
+
The `subscribe()` method accepts an event package name. The primary package:
|
|
512
|
+
|
|
513
|
+
| Package | Description |
|
|
514
|
+
|---------|-------------|
|
|
515
|
+
| `Advanced Call` | All call-related events (originate, ring, answer, hold, transfer, release, hook status, etc.) |
|
|
516
|
+
| `Call Center Queue` | Call Queue / ACD events (calls entering queue, queue state changes, etc.) |
|
|
517
|
+
|
|
518
|
+
> **Note:** wxcadm does not validate event package names — it passes whatever string you provide to the XSI server. The two packages shown above are confirmed in wxcadm examples. Additional BroadWorks-defined packages may be available depending on your org's configuration.
|
|
519
|
+
|
|
520
|
+
### Subscription Scopes
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
# Org-wide: monitor ALL calls across the entire organization
|
|
524
|
+
subscription = channel.subscribe(["Advanced Call"])
|
|
525
|
+
|
|
526
|
+
# Per-user: monitor calls for a specific person only
|
|
527
|
+
person = webex.org.get_person_by_email("user@domain.com")
|
|
528
|
+
subscription = channel.subscribe(["Advanced Call"], person=person)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Org-wide subscription uses the `serviceprovider/{enterpriseId}` endpoint. Per-user subscription uses the `user/{userId}` endpoint.
|
|
532
|
+
|
|
533
|
+
### XSI Event Types
|
|
534
|
+
|
|
535
|
+
A single outbound call from a Webex Calling desk phone generates **six events**:
|
|
536
|
+
|
|
537
|
+
| Event Type | When It Fires |
|
|
538
|
+
|------------|---------------|
|
|
539
|
+
| `xsi:HookStatusEvent` | Phone goes off-hook |
|
|
540
|
+
| `xsi:CallOriginatedEvent` | Call is initiated (dialing) |
|
|
541
|
+
| `xsi:CallUpdatedEvent` | Call state changes (ringing, etc.) |
|
|
542
|
+
| `xsi:CallAnsweredEvent` | Remote party answers |
|
|
543
|
+
| `xsi:CallReleasedEvent` | Call ends |
|
|
544
|
+
| `xsi:HookStatusEvent` | Phone goes on-hook |
|
|
545
|
+
|
|
546
|
+
Additional event types you will see in practice:
|
|
547
|
+
|
|
548
|
+
| Event Type | When It Fires |
|
|
549
|
+
|------------|---------------|
|
|
550
|
+
| `xsi:CallReceivedEvent` | Incoming call arrives |
|
|
551
|
+
| `xsi:CallHeldEvent` | Call placed on hold |
|
|
552
|
+
| `xsi:CallRetrievedEvent` | Call taken off hold |
|
|
553
|
+
| `xsi:CallTransferredEvent` | Call is transferred |
|
|
554
|
+
| `xsi:CallSubscriptionEvent` | Subscription state change |
|
|
555
|
+
| `xsi:ChannelTerminatedEvent` | Channel was closed (not queued — handled internally) |
|
|
556
|
+
| `xsi:ACDCallAddedEvent` | Call added to a Call Queue (requires "Call Center Queue" subscription) |
|
|
557
|
+
|
|
558
|
+
> **Note:** The event types listed above are confirmed from wxcadm source and documentation. The full set of possible event types is defined by the BroadWorks XSI schema and may include additional types not shown here. Use the `@xsi1:type` field in each event for runtime discovery.
|
|
559
|
+
|
|
560
|
+
### Event Payload Structure
|
|
561
|
+
|
|
562
|
+
Events are delivered as Python OrderedDicts (parsed from XML via `xmltodict`). The general structure:
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
{
|
|
566
|
+
'xsi:Event': {
|
|
567
|
+
'@xsi1:type': 'xsi:CallOriginatedEvent', # Event type
|
|
568
|
+
'xsi:eventID': '12345-abcde', # Unique event ID (used for ACK)
|
|
569
|
+
'xsi:sequenceNumber': '42', # Sequence number
|
|
570
|
+
'xsi:userId': 'user@domain.com', # User the event is for
|
|
571
|
+
'xsi:externalApplicationId': 'app-uuid', # Your application ID
|
|
572
|
+
'xsi:subscriptionId': 'sub-uuid', # Subscription that matched
|
|
573
|
+
'xsi:channelId': 'channel-uuid', # Channel it arrived on
|
|
574
|
+
'xsi:eventData': {
|
|
575
|
+
'@xsi1:type': 'xsi:CallEvent', # Data type
|
|
576
|
+
'xsi:call': {
|
|
577
|
+
'xsi:callId': 'callhalf-123',
|
|
578
|
+
'xsi:extTrackingId': 'tracking-456',
|
|
579
|
+
'xsi:personality': 'Originator', # or 'Terminator'
|
|
580
|
+
'xsi:state': 'Alerting', # Call state
|
|
581
|
+
'xsi:remoteParty': {
|
|
582
|
+
'xsi:address': {
|
|
583
|
+
'xsi:type': 'E164',
|
|
584
|
+
'#text': '+17192662837'
|
|
585
|
+
},
|
|
586
|
+
'xsi:callType': 'Network'
|
|
587
|
+
},
|
|
588
|
+
'xsi:startTime': '1710000000000',
|
|
589
|
+
'xsi:answerTime': '1710000005000',
|
|
590
|
+
# ... additional fields vary by event type
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Event Processing Best Practices
|
|
598
|
+
|
|
599
|
+
**Queue sizing:** A busy org can generate hundreds of events per second. Set `maxsize` large enough to absorb bursts but not so large it consumes all memory. Start with 500-1000 for a small org.
|
|
600
|
+
|
|
601
|
+
**Threaded consumer:** For production, process the queue in its own thread:
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
import queue
|
|
605
|
+
import threading
|
|
606
|
+
import wxcadm
|
|
607
|
+
|
|
608
|
+
def event_processor(q):
|
|
609
|
+
"""Process events in a dedicated thread."""
|
|
610
|
+
while True:
|
|
611
|
+
try:
|
|
612
|
+
event = q.get(timeout=5)
|
|
613
|
+
except queue.Empty:
|
|
614
|
+
continue
|
|
615
|
+
|
|
616
|
+
event_data = event['xsi:Event']
|
|
617
|
+
event_type = event_data['xsi:eventData']['@xsi1:type']
|
|
618
|
+
user_id = event_data.get('xsi:userId', 'unknown')
|
|
619
|
+
|
|
620
|
+
if event_type == 'xsi:CallOriginatedEvent':
|
|
621
|
+
handle_new_call(event_data)
|
|
622
|
+
elif event_type == 'xsi:CallAnsweredEvent':
|
|
623
|
+
handle_answered(event_data)
|
|
624
|
+
elif event_type == 'xsi:CallReleasedEvent':
|
|
625
|
+
handle_released(event_data)
|
|
626
|
+
# ... etc
|
|
627
|
+
|
|
628
|
+
q.task_done()
|
|
629
|
+
|
|
630
|
+
# Setup
|
|
631
|
+
webex = wxcadm.Webex(access_token, get_xsi=True)
|
|
632
|
+
events = wxcadm.XSIEvents(webex.org)
|
|
633
|
+
events_queue = queue.Queue(maxsize=1000)
|
|
634
|
+
channel = events.open_channel(events_queue)
|
|
635
|
+
channel.subscribe(["Advanced Call"])
|
|
636
|
+
|
|
637
|
+
# Start processor thread
|
|
638
|
+
processor = threading.Thread(target=event_processor, args=(events_queue,), daemon=True)
|
|
639
|
+
processor.start()
|
|
640
|
+
|
|
641
|
+
# Keep main thread alive
|
|
642
|
+
import time
|
|
643
|
+
while True:
|
|
644
|
+
time.sleep(60)
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## Channel Management
|
|
650
|
+
|
|
651
|
+
### Channel Lifecycle
|
|
652
|
+
|
|
653
|
+
When `open_channel()` is called, wxcadm:
|
|
654
|
+
|
|
655
|
+
1. Does a DNS SRV lookup: `_xsi-client._tcp.{xsi_domain}`
|
|
656
|
+
2. Opens one `XSIEventsChannel` per SRV record (per XSP server)
|
|
657
|
+
3. Each channel sends an XML `<Channel>` POST to `{endpoint}/v2.0/channel` with:
|
|
658
|
+
- `channelSetId` — groups channels together
|
|
659
|
+
- `applicationId` — unique per XSIEvents instance
|
|
660
|
+
- `priority=1`, `weight=50`, `expires=7200` (2 hours)
|
|
661
|
+
4. The POST is a **streaming request** — the connection stays open and events flow through it
|
|
662
|
+
5. A heartbeat thread sends `PUT .../channel/{id}/heartbeat` every 15 seconds
|
|
663
|
+
6. Channels auto-refresh every hour (before the 7200s expiry)
|
|
664
|
+
|
|
665
|
+
### Channel Failure Recovery
|
|
666
|
+
|
|
667
|
+
wxcadm has built-in resilience:
|
|
668
|
+
|
|
669
|
+
- If a channel fails to establish (no response within 40 seconds / 20 retries), it is marked inactive and a restart is attempted
|
|
670
|
+
- If a heartbeat gets HTTP 404 or 401, the channel is marked inactive and restarted
|
|
671
|
+
- If the streaming connection drops, the channel loop ends and triggers `restart_failed_channel()` (with a 60-second backoff)
|
|
672
|
+
- `restart_failed_channel()` creates a new channel to the same endpoint and sends a DELETE on the old one
|
|
673
|
+
|
|
674
|
+
### Checking Active Channels
|
|
675
|
+
|
|
676
|
+
```python
|
|
677
|
+
active = channel.active_channels # List of XSIEventsChannel with active=True
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Closing Everything
|
|
681
|
+
|
|
682
|
+
```python
|
|
683
|
+
channel.close() # Deletes all subscriptions, then deletes all channels
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## Subscription Management
|
|
689
|
+
|
|
690
|
+
### Subscribe
|
|
691
|
+
|
|
692
|
+
```python
|
|
693
|
+
# Org-wide
|
|
694
|
+
subscription = channel.subscribe(["Advanced Call"])
|
|
695
|
+
|
|
696
|
+
# Per-user
|
|
697
|
+
subscription = channel.subscribe(["Advanced Call"], person=some_person)
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Returns an `XSIEventsSubscription` instance (or `False` if subscription fails). The subscription has:
|
|
701
|
+
- `subscription.id` — server-assigned subscription ID
|
|
702
|
+
- `subscription.active` — boolean
|
|
703
|
+
- `subscription.event_package` — what was subscribed to
|
|
704
|
+
|
|
705
|
+
### Unsubscribe
|
|
706
|
+
|
|
707
|
+
```python
|
|
708
|
+
# Unsubscribe a specific subscription
|
|
709
|
+
channel.unsubscribe(subscription.id)
|
|
710
|
+
|
|
711
|
+
# Unsubscribe all
|
|
712
|
+
channel.unsubscribe("all")
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### Subscription Refresh
|
|
716
|
+
|
|
717
|
+
Subscriptions auto-refresh every hour (checked during heartbeat processing). The refresh sends a PUT with a new 7200-second expiry.
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## XSICallQueue: Call Queue Call Control
|
|
722
|
+
|
|
723
|
+
When monitoring XSI Events for a Call Queue, incoming calls have a `targetId` identifying the queue. Use `XSICallQueue` to control those calls:
|
|
724
|
+
|
|
725
|
+
```python
|
|
726
|
+
# Create a Call Queue controller from an event's targetId
|
|
727
|
+
call_queue = wxcadm.XSICallQueue(
|
|
728
|
+
call_queue_id="queue-id-from-event",
|
|
729
|
+
org=webex.org
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# Attach to a specific call (from event's callId)
|
|
733
|
+
call = call_queue.attach_call(call_id="call-id-from-event")
|
|
734
|
+
|
|
735
|
+
# Now you can control the call
|
|
736
|
+
call.transfer("2345") # Transfer to an agent
|
|
737
|
+
call.hangup() # Release the call
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
Call Queue calls support basic call control (transfer, hangup) but not all operations available to regular user calls. The wxcadm source confirms that Call Queue calls use a different URL base (`/v2.0/callcenter/{id}/calls` vs `/v2.0/user/{id}/calls`) and that the `transfer()` method sends the address as `phoneno` instead of `address` for Call Queue calls. The exact subset of operations the XSI server accepts on the callcenter endpoint is server-determined.
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## Monitoring Features (REST API)
|
|
745
|
+
|
|
746
|
+
Separate from XSI-Events, wxcadm provides a `MonitoringList` class for managing BLF (Busy Lamp Field) monitoring configurations through the Webex REST API:
|
|
747
|
+
|
|
748
|
+
```python
|
|
749
|
+
# Get a person's monitoring list
|
|
750
|
+
monitoring = person.monitoring # Returns MonitoringList
|
|
751
|
+
|
|
752
|
+
# Monitored elements (resolved to Person, Workspace, VirtualLine, or CallParkExtension)
|
|
753
|
+
for element in monitoring.monitored_elements:
|
|
754
|
+
print(element)
|
|
755
|
+
|
|
756
|
+
# Add a monitored element
|
|
757
|
+
monitoring.add(another_person)
|
|
758
|
+
|
|
759
|
+
# Remove a monitored element
|
|
760
|
+
monitoring.remove(another_person)
|
|
761
|
+
|
|
762
|
+
# Copy monitoring config to another user
|
|
763
|
+
monitoring.copy_to(other_person)
|
|
764
|
+
|
|
765
|
+
# Copy from another user
|
|
766
|
+
monitoring.copy_from(source_person)
|
|
767
|
+
|
|
768
|
+
# Replace entire monitoring config
|
|
769
|
+
monitoring.replace(other_monitoring_list)
|
|
770
|
+
|
|
771
|
+
# Clear all monitoring
|
|
772
|
+
monitoring.clear()
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
The `MonitoringList` resolves monitored element IDs to actual wxcadm objects (Person, Workspace, VirtualLine, CallParkExtension) automatically.
|
|
776
|
+
|
|
777
|
+
**Note:** This is the REST API `MonitoringList` class, not the XSI `monitoring` property. The XSI `person.xsi.monitoring` property reads BLF settings directly from the call control platform via XSI.
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
781
|
+
## Use Cases
|
|
782
|
+
|
|
783
|
+
### 1. Real-time Agent Dashboard
|
|
784
|
+
|
|
785
|
+
Monitor all call events org-wide, track which agents are on calls, display live call states:
|
|
786
|
+
|
|
787
|
+
```python
|
|
788
|
+
import queue
|
|
789
|
+
import wxcadm
|
|
790
|
+
|
|
791
|
+
webex = wxcadm.Webex(token, get_xsi=True)
|
|
792
|
+
events = wxcadm.XSIEvents(webex.org)
|
|
793
|
+
q = queue.Queue(maxsize=1000)
|
|
794
|
+
channel = events.open_channel(q)
|
|
795
|
+
channel.subscribe(["Advanced Call"])
|
|
796
|
+
|
|
797
|
+
active_calls = {} # Track calls by user
|
|
798
|
+
|
|
799
|
+
while True:
|
|
800
|
+
event = q.get()
|
|
801
|
+
evt = event['xsi:Event']
|
|
802
|
+
evt_type = evt['xsi:eventData']['@xsi1:type']
|
|
803
|
+
user = evt.get('xsi:userId', '')
|
|
804
|
+
|
|
805
|
+
if 'Originated' in evt_type or 'Received' in evt_type:
|
|
806
|
+
active_calls[user] = evt['xsi:eventData']
|
|
807
|
+
elif 'Released' in evt_type:
|
|
808
|
+
active_calls.pop(user, None)
|
|
809
|
+
|
|
810
|
+
# Push active_calls to your dashboard via WebSocket, SSE, etc.
|
|
811
|
+
update_dashboard(active_calls)
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
### 2. CRM Screen Pop
|
|
815
|
+
|
|
816
|
+
When a call comes in, look up the caller in your CRM and pop the record:
|
|
817
|
+
|
|
818
|
+
```python
|
|
819
|
+
while True:
|
|
820
|
+
event = q.get()
|
|
821
|
+
evt = event['xsi:Event']
|
|
822
|
+
if evt['xsi:eventData']['@xsi1:type'] == 'xsi:CallReceivedEvent':
|
|
823
|
+
caller = evt['xsi:eventData']['xsi:call']['xsi:remoteParty']
|
|
824
|
+
phone = caller['xsi:address'].get('#text', '')
|
|
825
|
+
agent = evt.get('xsi:userId', '')
|
|
826
|
+
crm_record = lookup_crm(phone)
|
|
827
|
+
send_screen_pop(agent, crm_record)
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### 3. Call Center Queue Management
|
|
831
|
+
|
|
832
|
+
Monitor Call Queue events and programmatically route calls:
|
|
833
|
+
|
|
834
|
+
```python
|
|
835
|
+
while True:
|
|
836
|
+
event = q.get()
|
|
837
|
+
evt = event['xsi:Event']
|
|
838
|
+
target_id = evt.get('xsi:targetId', '')
|
|
839
|
+
|
|
840
|
+
if is_call_queue(target_id):
|
|
841
|
+
call_id = evt['xsi:eventData']['xsi:call']['xsi:callId']
|
|
842
|
+
cq = wxcadm.XSICallQueue(target_id, webex.org)
|
|
843
|
+
call = cq.attach_call(call_id)
|
|
844
|
+
|
|
845
|
+
# Route based on business logic
|
|
846
|
+
best_agent = find_available_agent()
|
|
847
|
+
call.transfer(best_agent.extension)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### 4. Compliance / Call Logging
|
|
851
|
+
|
|
852
|
+
Capture every call event for audit:
|
|
853
|
+
|
|
854
|
+
```python
|
|
855
|
+
import json
|
|
856
|
+
from datetime import datetime
|
|
857
|
+
|
|
858
|
+
while True:
|
|
859
|
+
event = q.get()
|
|
860
|
+
evt = event['xsi:Event']
|
|
861
|
+
log_entry = {
|
|
862
|
+
'timestamp': datetime.utcnow().isoformat(),
|
|
863
|
+
'event_type': evt['xsi:eventData']['@xsi1:type'],
|
|
864
|
+
'user': evt.get('xsi:userId', ''),
|
|
865
|
+
'event_id': evt.get('xsi:eventID', ''),
|
|
866
|
+
'raw': json.dumps(event, default=str)
|
|
867
|
+
}
|
|
868
|
+
write_to_audit_log(log_entry)
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### 5. Click-to-Dial Integration
|
|
872
|
+
|
|
873
|
+
Embed calling into any web app or tool:
|
|
874
|
+
|
|
875
|
+
```python
|
|
876
|
+
def click_to_dial(agent_email: str, destination: str):
|
|
877
|
+
person = webex.org.get_person_by_email(agent_email)
|
|
878
|
+
person.start_xsi()
|
|
879
|
+
call = person.xsi.new_call(address=destination)
|
|
880
|
+
return call.id # Return call ID for tracking/control
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### 6. Automated Call Recording Control
|
|
884
|
+
|
|
885
|
+
Start/stop recording based on business rules:
|
|
886
|
+
|
|
887
|
+
```python
|
|
888
|
+
while True:
|
|
889
|
+
event = q.get()
|
|
890
|
+
evt = event['xsi:Event']
|
|
891
|
+
if evt['xsi:eventData']['@xsi1:type'] == 'xsi:CallAnsweredEvent':
|
|
892
|
+
user_id = evt.get('xsi:userId', '')
|
|
893
|
+
call_id = evt['xsi:eventData']['xsi:call']['xsi:callId']
|
|
894
|
+
|
|
895
|
+
if should_record(user_id):
|
|
896
|
+
person = webex.org.get_person_by_email(user_id)
|
|
897
|
+
person.start_xsi()
|
|
898
|
+
call = person.xsi.attach_call(call_id)
|
|
899
|
+
call.recording("start")
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## Class Reference Summary
|
|
905
|
+
|
|
906
|
+
| Class | Purpose | Key Methods/Properties |
|
|
907
|
+
|-------|---------|----------------------|
|
|
908
|
+
| `XSI` | Per-user XSI session | `new_call()`, `answer_call()`, `calls`, `profile`, `directory()`, `get_services()`, `attach_call()` |
|
|
909
|
+
| `XSIEvents` | Org-level event manager | `open_channel(queue)` |
|
|
910
|
+
| `XSIEventsChannelSet` | Channel group + subscriptions | `subscribe()`, `unsubscribe()`, `close()`, `active_channels` |
|
|
911
|
+
| `XSIEventsChannel` | Single streaming connection | Managed automatically; `active`, `delete()` |
|
|
912
|
+
| `XSIEventsSubscription` | Event subscription | `id`, `active`, `delete()` |
|
|
913
|
+
| `XSICallQueue` | Call Queue call control | `attach_call(call_id)` |
|
|
914
|
+
| `Call` | Individual call control | `originate()`, `hold()`, `resume()`, `transfer()`, `finish_transfer()`, `conference()`, `park()`, `hangup()`, `status`, `recording()`, `send_dtmf()`, `exec_push()`, `answer()`, `reconnect()` |
|
|
915
|
+
| `Conference` | Multi-party conference | `mute(call_id)`, `deaf(call_id)` |
|
|
916
|
+
| `XSIResponse` | API response wrapper | `success`, `summary`, `raw_response`, truthy/falsy |
|
|
917
|
+
| `FeatureAccessCode` | FAC definition | `feature`, `code`, `alternate_code` |
|
|
918
|
+
| `MonitoringList` | BLF monitoring config (REST) | `add()`, `remove()`, `replace()`, `copy_to()`, `copy_from()`, `clear()` |
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
## Gotchas & Tips
|
|
923
|
+
|
|
924
|
+
1. **XSI must be enabled by Cisco TAC** before any of this works. There is no self-service toggle.
|
|
925
|
+
|
|
926
|
+
2. **Events volume is high.** A single call generates 6 events. A busy org with 100 concurrent calls can produce hundreds of events per second. Size your queue and process events quickly.
|
|
927
|
+
|
|
928
|
+
3. **Events are XML, not JSON.** wxcadm parses them with `xmltodict` into OrderedDicts. The `$` key holds text content (e.g., `config['code']['$']`). The `@` prefix denotes attributes.
|
|
929
|
+
|
|
930
|
+
4. **ChannelTerminatedEvent is NOT queued.** wxcadm filters this out before putting events in your queue, since it's an infrastructure event, not a business event.
|
|
931
|
+
|
|
932
|
+
5. **Channel auto-recovery is built in** but not instant. A failed channel waits 60 seconds before restarting. During that window, events for that XSP are lost.
|
|
933
|
+
|
|
934
|
+
6. **Heartbeats every 15 seconds.** If a heartbeat fails, the retry interval drops to 10 seconds. If it gets a 404/401, the channel restarts.
|
|
935
|
+
|
|
936
|
+
7. **7200-second expiry.** Both channels and subscriptions expire after 2 hours. wxcadm auto-refreshes them hourly.
|
|
937
|
+
|
|
938
|
+
8. **XSI profile vs. REST profile.** The XSI profile comes from the call control back-end and may differ from the REST API profile (which comes from Common Identity / Active Directory).
|
|
939
|
+
|
|
940
|
+
9. **Call IDs are transient.** They only exist while the call is active. Use `attach_call()` promptly after receiving an event.
|
|
941
|
+
|
|
942
|
+
10. **The `recording()` method has a bug (verified 2026-03-18).** Line 1528 has a duplicate `elif action.lower() == "resume"` that should be `"pause"`. The first `"resume"` (line 1524, → `ResumeRecording`) works correctly. The duplicate (line 1528, → `PauseRecording`) is dead code. As a result, `recording("pause")` raises `ValueError`. Workaround: call `PauseRecording` XSI endpoint directly.
|
|
943
|
+
|
|
944
|
+
11. **SRV lookup dependency.** wxcadm uses the `srvlookup` library to find XSP servers. If DNS SRV records are unreachable, channel creation will fail.
|
|
945
|
+
|
|
946
|
+
12. **Session cookies.** Each channel uses a `requests.Session` and preserves cookies from the initial streaming POST. This ensures subsequent heartbeats and ACKs hit the correct server instance behind load balancers.
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## See Also
|
|
951
|
+
|
|
952
|
+
- [call-control.md](call-control.md) — wxc_sdk call control APIs (REST-based, not XSI) for comparison with wxcadm's XSI call control
|
|
953
|
+
- [webhooks-events.md](webhooks-events.md) — wxc_sdk webhook and event subscription patterns (REST-based alternative to XSI-Events for some use cases)
|