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,243 @@
|
|
|
1
|
+
"""Voicemail extractor for CUCM AXL.
|
|
2
|
+
|
|
3
|
+
Pulls Voicemail Profiles and Voicemail Pilots from CUCM AXL.
|
|
4
|
+
Optionally extracts per-user Unity Connection settings via CUPI.
|
|
5
|
+
|
|
6
|
+
Sources:
|
|
7
|
+
- 02b-cucm-extraction.md §2.6 (voicemail extraction)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from wxcli.migration.cucm.connection import AXLConnection
|
|
16
|
+
from wxcli.migration.cucm.extractors.base import BaseExtractor, ExtractionResult
|
|
17
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value, to_list
|
|
18
|
+
from wxcli.migration.cucm.unity_connection import UnityConnectionClient
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------
|
|
23
|
+
# ReturnedTags constants
|
|
24
|
+
# ------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
# Verified: listVoiceMailProfile does NOT accept voiceMailPilot.
|
|
27
|
+
# Use getVoiceMailProfile for pilot reference.
|
|
28
|
+
VOICEMAIL_PROFILE_LIST_RETURNED_TAGS = {
|
|
29
|
+
"name": "", "description": "", "voiceMailboxMask": "",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Verified: listVoiceMailPilot CSS field is 'cssName', NOT 'callingSearchSpaceName'.
|
|
33
|
+
VOICEMAIL_PILOT_LIST_RETURNED_TAGS = {
|
|
34
|
+
"dirn": "", "description": "",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VoicemailExtractor(BaseExtractor):
|
|
39
|
+
"""Extract voicemail configuration from CUCM AXL.
|
|
40
|
+
|
|
41
|
+
Covers Voicemail Profiles and Voicemail Pilots.
|
|
42
|
+
Optionally fetches per-user voicemail settings from Unity Connection
|
|
43
|
+
via the CUPI REST API when a ``unity_client`` is provided.
|
|
44
|
+
|
|
45
|
+
(from 02b §2.6)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name = "voicemail"
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
connection: AXLConnection,
|
|
53
|
+
unity_client: UnityConnectionClient | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(connection)
|
|
56
|
+
self.unity_client = unity_client
|
|
57
|
+
self.results: dict[str, list[dict[str, Any]]] = {}
|
|
58
|
+
|
|
59
|
+
def extract(self) -> ExtractionResult:
|
|
60
|
+
"""Run voicemail extraction (AXL profiles and pilots).
|
|
61
|
+
|
|
62
|
+
Unity Connection per-user settings are extracted separately via
|
|
63
|
+
``extract_unity_settings()`` because the user list comes from
|
|
64
|
+
the discovery extractor.
|
|
65
|
+
|
|
66
|
+
Returns an ExtractionResult summarizing total objects and errors.
|
|
67
|
+
"""
|
|
68
|
+
result = ExtractionResult(extractor=self.name)
|
|
69
|
+
|
|
70
|
+
self.results["voicemail_profiles"] = self._extract_voicemail_profiles(
|
|
71
|
+
result
|
|
72
|
+
)
|
|
73
|
+
self.results["voicemail_pilots"] = self._extract_voicemail_pilots(
|
|
74
|
+
result
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Shared/group mailboxes (Unity Connection only — skipped without client)
|
|
78
|
+
self.results["shared_mailboxes"] = self._extract_shared_mailboxes(result)
|
|
79
|
+
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Voicemail Profiles
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def _extract_voicemail_profiles(
|
|
87
|
+
self, result: ExtractionResult
|
|
88
|
+
) -> list[dict[str, Any]]:
|
|
89
|
+
"""List all voicemail profiles, then get full detail for each.
|
|
90
|
+
|
|
91
|
+
Verified against live CUCM 15.0 (2026-03-23):
|
|
92
|
+
- listVoiceMailProfile method confirmed
|
|
93
|
+
- voiceMailPilot reference NOT returned by list — requires getVoiceMailProfile
|
|
94
|
+
"""
|
|
95
|
+
profiles: list[dict[str, Any]] = []
|
|
96
|
+
try:
|
|
97
|
+
summaries = self.paginated_list(
|
|
98
|
+
"listVoiceMailProfile",
|
|
99
|
+
{"name": "%"},
|
|
100
|
+
VOICEMAIL_PROFILE_LIST_RETURNED_TAGS,
|
|
101
|
+
)
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
msg = f"listVoiceMailProfile failed: {exc}"
|
|
104
|
+
logger.error("[%s] %s", self.name, msg)
|
|
105
|
+
result.errors.append(msg)
|
|
106
|
+
return profiles
|
|
107
|
+
|
|
108
|
+
for summary in summaries:
|
|
109
|
+
result.total += 1
|
|
110
|
+
profile_name = summary.get("name", "")
|
|
111
|
+
if not profile_name:
|
|
112
|
+
result.failed += 1
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
detail = self.get_detail("getVoiceMailProfile", name=profile_name)
|
|
116
|
+
except Exception as exc:
|
|
117
|
+
logger.warning(
|
|
118
|
+
"[%s] getVoiceMailProfile error for %s: %s",
|
|
119
|
+
self.name, profile_name, exc,
|
|
120
|
+
)
|
|
121
|
+
profiles.append(summary)
|
|
122
|
+
continue
|
|
123
|
+
if detail is None:
|
|
124
|
+
profiles.append(summary)
|
|
125
|
+
continue
|
|
126
|
+
profiles.append(detail)
|
|
127
|
+
|
|
128
|
+
return profiles
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
# Voicemail Pilots
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
def _extract_voicemail_pilots(
|
|
135
|
+
self, result: ExtractionResult
|
|
136
|
+
) -> list[dict[str, Any]]:
|
|
137
|
+
"""List all voicemail pilots.
|
|
138
|
+
|
|
139
|
+
Verified against live CUCM 15.0 2026-03-23: search field is 'dirn'.
|
|
140
|
+
"""
|
|
141
|
+
pilots: list[dict[str, Any]] = []
|
|
142
|
+
try:
|
|
143
|
+
summaries = self.paginated_list(
|
|
144
|
+
"listVoiceMailPilot",
|
|
145
|
+
{"dirn": "%"},
|
|
146
|
+
VOICEMAIL_PILOT_LIST_RETURNED_TAGS,
|
|
147
|
+
)
|
|
148
|
+
pilots.extend(summaries)
|
|
149
|
+
result.total += len(summaries)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
msg = f"listVoiceMailPilot failed: {exc}"
|
|
152
|
+
logger.error("[%s] %s", self.name, msg)
|
|
153
|
+
result.errors.append(msg)
|
|
154
|
+
|
|
155
|
+
return pilots
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Unity Connection shared mailboxes
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
def _extract_shared_mailboxes(
|
|
162
|
+
self, result: ExtractionResult
|
|
163
|
+
) -> list[dict[str, Any]]:
|
|
164
|
+
"""Fetch Unity Connection shared/group call handlers via CUPI.
|
|
165
|
+
|
|
166
|
+
Returns an empty list when no Unity Connection client was
|
|
167
|
+
configured. Errors are logged but do not fail discovery.
|
|
168
|
+
"""
|
|
169
|
+
if self.unity_client is None:
|
|
170
|
+
logger.info(
|
|
171
|
+
"[%s] No Unity Connection client — "
|
|
172
|
+
"skipping shared mailbox extraction",
|
|
173
|
+
self.name,
|
|
174
|
+
)
|
|
175
|
+
return []
|
|
176
|
+
try:
|
|
177
|
+
mailboxes = self.unity_client.extract_shared_mailboxes()
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
msg = f"Unity shared mailbox extraction failed: {exc}"
|
|
180
|
+
logger.error("[%s] %s", self.name, msg)
|
|
181
|
+
result.errors.append(msg)
|
|
182
|
+
return []
|
|
183
|
+
result.total += len(mailboxes)
|
|
184
|
+
return mailboxes
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------
|
|
187
|
+
# Unity Connection per-user settings
|
|
188
|
+
# ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def extract_unity_settings(
|
|
191
|
+
self, user_ids: list[str]
|
|
192
|
+
) -> dict[str, dict[str, Any]]:
|
|
193
|
+
"""Extract per-user voicemail settings from Unity Connection.
|
|
194
|
+
|
|
195
|
+
This is called separately from ``extract()`` because the user
|
|
196
|
+
list comes from the discovery extractor (02b §2.1), not from
|
|
197
|
+
this extractor.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
user_ids: List of CUCM user IDs (aliases) to look up in
|
|
201
|
+
Unity Connection.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Dict mapping CUCM user ID to their Unity Connection
|
|
205
|
+
voicemail settings. Users not found in UC are omitted.
|
|
206
|
+
"""
|
|
207
|
+
if self.unity_client is None:
|
|
208
|
+
logger.info(
|
|
209
|
+
"[%s] No Unity Connection client configured — "
|
|
210
|
+
"skipping per-user VM extraction",
|
|
211
|
+
self.name,
|
|
212
|
+
)
|
|
213
|
+
return {}
|
|
214
|
+
|
|
215
|
+
settings: dict[str, dict[str, Any]] = {}
|
|
216
|
+
for user_id in user_ids:
|
|
217
|
+
try:
|
|
218
|
+
user_settings = self.unity_client.extract_user_vm_settings(
|
|
219
|
+
user_id
|
|
220
|
+
)
|
|
221
|
+
if user_settings is not None:
|
|
222
|
+
settings[user_id] = user_settings
|
|
223
|
+
else:
|
|
224
|
+
logger.debug(
|
|
225
|
+
"[%s] User %s not found in Unity Connection",
|
|
226
|
+
self.name,
|
|
227
|
+
user_id,
|
|
228
|
+
)
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
logger.warning(
|
|
231
|
+
"[%s] Unity extraction failed for user %s: %s",
|
|
232
|
+
self.name,
|
|
233
|
+
user_id,
|
|
234
|
+
exc,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
logger.info(
|
|
238
|
+
"[%s] Extracted Unity settings for %d/%d users",
|
|
239
|
+
self.name,
|
|
240
|
+
len(settings),
|
|
241
|
+
len(user_ids),
|
|
242
|
+
)
|
|
243
|
+
return settings
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Workspace (common-area phone) classifier post-processor.
|
|
2
|
+
|
|
3
|
+
Identifies phones that are common-area devices suitable for Webex Calling
|
|
4
|
+
workspace provisioning. Runs as a pass 2 helper against already-extracted
|
|
5
|
+
phone data -- does NOT make AXL calls.
|
|
6
|
+
|
|
7
|
+
Sources:
|
|
8
|
+
- 02b-cucm-extraction.md S2.8 (workspace classification)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from wxcli.migration.cucm.extractors.helpers import ref_value
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def is_common_area_device(phone_raw: dict[str, Any]) -> bool:
|
|
22
|
+
"""Determine if a phone dict represents a common-area device.
|
|
23
|
+
|
|
24
|
+
Classification logic (from 02b S2.8):
|
|
25
|
+
- Must have no ownerUserName (unowned device).
|
|
26
|
+
- Must have device class 'Phone' (excludes CTI ports, route points, etc.).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
phone_raw: Raw phone dict from DeviceExtractor.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
True if the phone is a common-area device.
|
|
33
|
+
"""
|
|
34
|
+
# ownerUserName may be a reference dict {'_value_1': 'user', 'uuid': '...'}
|
|
35
|
+
# or a plain string. Use ref_value to handle both.
|
|
36
|
+
owner = ref_value(phone_raw.get("ownerUserName"))
|
|
37
|
+
if owner is not None:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
device_class = phone_raw.get("class", "")
|
|
41
|
+
if device_class not in ("Phone",):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class WorkspaceClassifier:
|
|
48
|
+
"""Classify common-area phones from DeviceExtractor phone data.
|
|
49
|
+
|
|
50
|
+
This is a pass 2 helper, NOT a BaseExtractor subclass. It operates
|
|
51
|
+
entirely on in-memory phone dicts and returns the subset that qualify
|
|
52
|
+
as common-area devices.
|
|
53
|
+
|
|
54
|
+
(from 02b S2.8)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, phones: list[dict]) -> None:
|
|
58
|
+
self.phones = phones
|
|
59
|
+
|
|
60
|
+
def classify(self) -> list[dict[str, Any]]:
|
|
61
|
+
"""Return the subset of phones that are common-area devices.
|
|
62
|
+
|
|
63
|
+
Returns the raw phone dicts unchanged -- transformation is the
|
|
64
|
+
normalizer's job.
|
|
65
|
+
|
|
66
|
+
Note: ATA devices (name starts with 'ATA') pass the classification
|
|
67
|
+
since they are technically common-area, but may be incompatible with
|
|
68
|
+
Webex Calling workspaces. Downstream stages should flag these.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of raw phone dicts that are common-area devices.
|
|
72
|
+
"""
|
|
73
|
+
results = [phone for phone in self.phones if is_common_area_device(phone)]
|
|
74
|
+
|
|
75
|
+
ata_count = sum(1 for p in results if p.get("name", "").startswith("ATA"))
|
|
76
|
+
if ata_count:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"Classified %d ATA device(s) as common-area -- these may be "
|
|
79
|
+
"incompatible with Webex Calling workspaces",
|
|
80
|
+
ata_count,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
logger.info(
|
|
84
|
+
"Workspace classification complete: %d of %d phones are common-area",
|
|
85
|
+
len(results),
|
|
86
|
+
len(self.phones),
|
|
87
|
+
)
|
|
88
|
+
return results
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""CUPI REST client for Cisco Unity Connection.
|
|
2
|
+
|
|
3
|
+
Retrieves per-user voicemail settings that are not available via CUCM AXL.
|
|
4
|
+
|
|
5
|
+
Sources:
|
|
6
|
+
- 02b-cucm-extraction.md §2.6 (CUPI connection setup, endpoints, field mapping)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from requests.auth import HTTPBasicAuth
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UnityConnectionError(Exception):
|
|
21
|
+
"""Raised when the Unity Connection client cannot connect or authenticate."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnityConnectionClient:
|
|
25
|
+
"""CUPI REST client for Unity Connection per-user voicemail settings.
|
|
26
|
+
|
|
27
|
+
(from 02b §2.6: CUPI connection setup)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
host: Unity Connection hostname or IP.
|
|
31
|
+
username: Admin username.
|
|
32
|
+
password: Admin password.
|
|
33
|
+
verify_ssl: Whether to verify SSL certificates (default False).
|
|
34
|
+
timeout: Request timeout in seconds (default 30).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
host: str,
|
|
40
|
+
username: str,
|
|
41
|
+
password: str,
|
|
42
|
+
verify_ssl: bool = False,
|
|
43
|
+
timeout: int = 30,
|
|
44
|
+
) -> None:
|
|
45
|
+
self.base_url = f"https://{host}/vmrest"
|
|
46
|
+
self.session = requests.Session()
|
|
47
|
+
self.session.auth = HTTPBasicAuth(username, password)
|
|
48
|
+
self.session.verify = verify_ssl
|
|
49
|
+
self.session.timeout = timeout
|
|
50
|
+
self.session.headers.update(
|
|
51
|
+
{
|
|
52
|
+
"Accept": "application/json",
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# User lookup
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def get_user_by_alias(self, alias: str) -> dict[str, Any] | None:
|
|
62
|
+
"""Look up UC user by CUCM userid/alias.
|
|
63
|
+
|
|
64
|
+
(from 02b §2.6: CUPI user lookup)
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
resp = self.session.get(
|
|
69
|
+
f"{self.base_url}/users",
|
|
70
|
+
params={"query": f"(alias is {alias})"},
|
|
71
|
+
)
|
|
72
|
+
if resp.status_code == 200:
|
|
73
|
+
data = resp.json()
|
|
74
|
+
users = data.get("User", [])
|
|
75
|
+
if isinstance(users, dict):
|
|
76
|
+
users = [users]
|
|
77
|
+
return users[0] if users else None
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
logger.warning("CUPI user lookup failed for %s: %s", alias, exc)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# Per-user voicemail settings
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def get_notification_devices(self, object_id: str) -> list[dict[str, Any]]:
|
|
87
|
+
"""Get notification devices for a user.
|
|
88
|
+
|
|
89
|
+
(from 02b §2.6: notifications.enabled, notifications.destination)
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
return self._get_list(
|
|
93
|
+
f"/users/{object_id}/notificationdevices",
|
|
94
|
+
"NotificationDevice",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def get_call_handler(self, handler_id: str) -> dict[str, Any] | None:
|
|
98
|
+
"""Get call handler (greeting/transfer rules).
|
|
99
|
+
|
|
100
|
+
(from 02b §2.6: sendAllCalls, sendBusyCalls, sendUnansweredCalls)
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
return self._get(f"/handlers/callhandlers/{handler_id}")
|
|
104
|
+
|
|
105
|
+
def get_message_store_settings(
|
|
106
|
+
self, object_id: str
|
|
107
|
+
) -> dict[str, Any] | None:
|
|
108
|
+
"""Get message store settings for a user.
|
|
109
|
+
|
|
110
|
+
(from 02b §2.6: storageType, mwiEnabled, externalEmail)
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
return self._get(f"/users/{object_id}/messagestoresettings")
|
|
114
|
+
|
|
115
|
+
def get_transfer_rule_zero(self, object_id: str) -> dict[str, Any] | None:
|
|
116
|
+
"""Get transfer-to-zero rule for a user.
|
|
117
|
+
|
|
118
|
+
(from 02b §2.6: transferToNumber settings)
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
return self._get(f"/users/{object_id}/transferrules/transferzero")
|
|
122
|
+
|
|
123
|
+
def get_fax_settings(self, object_id: str) -> dict[str, Any] | None:
|
|
124
|
+
"""Get fax server settings for a user.
|
|
125
|
+
|
|
126
|
+
(from 02b §2.6: fax message settings)
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
return self._get(f"/users/{object_id}/faxserversettings")
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------------------------------
|
|
132
|
+
# Aggregate extraction
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def extract_user_vm_settings(
|
|
136
|
+
self, cucm_userid: str
|
|
137
|
+
) -> dict[str, Any] | None:
|
|
138
|
+
"""Extract all voicemail settings for one user.
|
|
139
|
+
|
|
140
|
+
Looks up the user by alias, then fetches all per-user settings.
|
|
141
|
+
Returns a consolidated dict, or None if user not found in UC.
|
|
142
|
+
|
|
143
|
+
(from 02b §2.6: extraction flow steps 3-4)
|
|
144
|
+
"""
|
|
145
|
+
uc_user = self.get_user_by_alias(cucm_userid)
|
|
146
|
+
if uc_user is None:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
object_id = uc_user.get("ObjectId", "")
|
|
150
|
+
call_handler_id = uc_user.get("CallhandlerObjectId", "")
|
|
151
|
+
|
|
152
|
+
result: dict[str, Any] = {
|
|
153
|
+
"uc_object_id": object_id,
|
|
154
|
+
"vm_enabled": True,
|
|
155
|
+
"call_handler_id": call_handler_id,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Notification devices
|
|
159
|
+
notif_devices = self.get_notification_devices(object_id)
|
|
160
|
+
result["notification_enabled"] = any(
|
|
161
|
+
d.get("Enabled", False) for d in notif_devices
|
|
162
|
+
)
|
|
163
|
+
email_devices = [
|
|
164
|
+
d for d in notif_devices if d.get("SmtpAddress")
|
|
165
|
+
]
|
|
166
|
+
result["notification_destination"] = (
|
|
167
|
+
email_devices[0].get("SmtpAddress") if email_devices else None
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Call handler transfer rules
|
|
171
|
+
if call_handler_id:
|
|
172
|
+
handler = self.get_call_handler(call_handler_id)
|
|
173
|
+
if handler:
|
|
174
|
+
result["send_all_calls"] = _to_bool(
|
|
175
|
+
handler.get("PlayTransferPrompt")
|
|
176
|
+
)
|
|
177
|
+
result["send_busy_calls"] = _to_bool(
|
|
178
|
+
handler.get("SendBusyCalls")
|
|
179
|
+
)
|
|
180
|
+
result["send_unanswered_calls"] = _to_bool(
|
|
181
|
+
handler.get("SendUnansweredCalls")
|
|
182
|
+
)
|
|
183
|
+
result["unanswered_rings"] = handler.get("RingsToWait")
|
|
184
|
+
result["busy_greeting_type"] = (
|
|
185
|
+
"CUSTOM"
|
|
186
|
+
if handler.get("PlayPostGreetingRecording")
|
|
187
|
+
else "DEFAULT"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Message store settings
|
|
191
|
+
msg_store = self.get_message_store_settings(object_id)
|
|
192
|
+
if msg_store:
|
|
193
|
+
result["storage_type"] = msg_store.get("StorageType", "INTERNAL")
|
|
194
|
+
result["mwi_enabled"] = _to_bool(msg_store.get("MwiEnabled"))
|
|
195
|
+
result["external_email"] = msg_store.get("ExternalEmail")
|
|
196
|
+
result["email_copy_enabled"] = _to_bool(
|
|
197
|
+
msg_store.get("SendCopyToEmail")
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Transfer to zero
|
|
201
|
+
t_zero = self.get_transfer_rule_zero(object_id)
|
|
202
|
+
if t_zero:
|
|
203
|
+
result["transfer_to_zero_enabled"] = _to_bool(
|
|
204
|
+
t_zero.get("Enabled")
|
|
205
|
+
)
|
|
206
|
+
result["transfer_to_zero_destination"] = t_zero.get(
|
|
207
|
+
"Extension"
|
|
208
|
+
) or t_zero.get("PhoneNumber")
|
|
209
|
+
|
|
210
|
+
# Fax settings
|
|
211
|
+
fax = self.get_fax_settings(object_id)
|
|
212
|
+
if fax:
|
|
213
|
+
result["fax_enabled"] = _to_bool(fax.get("Enabled"))
|
|
214
|
+
result["fax_number"] = fax.get("FaxNumber")
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Shared/group mailbox discovery
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
def extract_shared_mailboxes(self) -> list[dict[str, Any]]:
|
|
223
|
+
"""Extract Unity Connection shared/group call handlers.
|
|
224
|
+
|
|
225
|
+
Queries /vmrest/handlers/callhandlers and filters to non-primary
|
|
226
|
+
handlers (IsPrimary=false). Primary handlers are per-user mailboxes
|
|
227
|
+
already covered by extract_user_vm_settings. Non-primary handlers
|
|
228
|
+
are shared/group mailboxes (sales@, support@, etc.).
|
|
229
|
+
|
|
230
|
+
Returns an empty list on error or when CUPI is unreachable —
|
|
231
|
+
callers should treat absence as "Unity Connection not available"
|
|
232
|
+
rather than "no shared mailboxes exist".
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
resp = self.session.get(
|
|
236
|
+
f"{self.base_url}/handlers/callhandlers",
|
|
237
|
+
)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
logger.warning("CUPI shared-mailbox query failed: %s", exc)
|
|
240
|
+
return []
|
|
241
|
+
|
|
242
|
+
if resp.status_code != 200:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"CUPI shared-mailbox query returned %s", resp.status_code
|
|
245
|
+
)
|
|
246
|
+
return []
|
|
247
|
+
|
|
248
|
+
data = resp.json() if callable(getattr(resp, "json", None)) else {}
|
|
249
|
+
handlers = data.get("Callhandler", [])
|
|
250
|
+
# CUPI returns a bare dict when exactly one handler exists.
|
|
251
|
+
if isinstance(handlers, dict):
|
|
252
|
+
handlers = [handlers]
|
|
253
|
+
|
|
254
|
+
# Filter out primary (per-user) handlers — we only want shared ones.
|
|
255
|
+
shared: list[dict[str, Any]] = []
|
|
256
|
+
for h in handlers:
|
|
257
|
+
if not isinstance(h, dict):
|
|
258
|
+
continue
|
|
259
|
+
is_primary = str(h.get("IsPrimary", "false")).lower() == "true"
|
|
260
|
+
if is_primary:
|
|
261
|
+
continue
|
|
262
|
+
shared.append(h)
|
|
263
|
+
|
|
264
|
+
logger.info(
|
|
265
|
+
"Extracted %d Unity Connection shared mailbox(es)", len(shared)
|
|
266
|
+
)
|
|
267
|
+
return shared
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Internal helpers
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def _get(self, path: str) -> dict[str, Any] | None:
|
|
274
|
+
"""GET a single CUPI resource."""
|
|
275
|
+
try:
|
|
276
|
+
resp = self.session.get(f"{self.base_url}{path}")
|
|
277
|
+
if resp.status_code == 200:
|
|
278
|
+
return resp.json()
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
logger.warning("CUPI GET %s failed: %s", path, exc)
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
def _get_list(
|
|
284
|
+
self, path: str, key: str
|
|
285
|
+
) -> list[dict[str, Any]]:
|
|
286
|
+
"""GET a CUPI collection resource."""
|
|
287
|
+
data = self._get(path)
|
|
288
|
+
if data is None:
|
|
289
|
+
return []
|
|
290
|
+
items = data.get(key, [])
|
|
291
|
+
if isinstance(items, dict):
|
|
292
|
+
items = [items]
|
|
293
|
+
return items
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _to_bool(value: Any) -> bool | None:
|
|
297
|
+
"""Convert CUPI response values to bool."""
|
|
298
|
+
if value is None:
|
|
299
|
+
return None
|
|
300
|
+
if isinstance(value, bool):
|
|
301
|
+
return value
|
|
302
|
+
if isinstance(value, str):
|
|
303
|
+
return value.lower() in ("true", "1", "yes")
|
|
304
|
+
return bool(value)
|