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,347 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typer
|
|
3
|
+
from wxcli.auth import get_api
|
|
4
|
+
from wxcli.errors import WebexError, handle_rest_error
|
|
5
|
+
from wxcli.output import print_table, print_json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Manage Webex Calling converged-recordings.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("list")
|
|
12
|
+
def cmd_list(
|
|
13
|
+
from_param: str = typer.Option(None, "--from", help="Starting date and time (inclusive) for recordings to return,"),
|
|
14
|
+
to: str = typer.Option(None, "--to", help="Ending date and time (exclusive) for List recordings to retu"),
|
|
15
|
+
status: str = typer.Option(None, "--status", help="Choices: available, deleted"),
|
|
16
|
+
service_type: str = typer.Option(None, "--service-type", help="Choices: calling, customerAssist"),
|
|
17
|
+
format_param: str = typer.Option(None, "--format", help="Choices: MP3"),
|
|
18
|
+
owner_type: str = typer.Option(None, "--owner-type", help="Choices: user, place, virtualLine, callQueue"),
|
|
19
|
+
storage_region: str = typer.Option(None, "--storage-region", help="Choices: US, SG, GB, JP, DE, AU, IN, CA"),
|
|
20
|
+
location_id: str = typer.Option(None, "--location-id", help="Fetch recordings for users in a particular Webex Calling loc"),
|
|
21
|
+
topic: str = typer.Option(None, "--topic", help="Recording's topic. If specified, the API filters recordings"),
|
|
22
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
23
|
+
limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
|
|
24
|
+
offset: int = typer.Option(0, "--offset", help="Start offset"),
|
|
25
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
26
|
+
):
|
|
27
|
+
"""List Recordings."""
|
|
28
|
+
api = get_api(debug=debug)
|
|
29
|
+
url = f"https://webexapis.com/v1/convergedRecordings"
|
|
30
|
+
params = {}
|
|
31
|
+
if from_param is not None:
|
|
32
|
+
params["from"] = from_param
|
|
33
|
+
if to is not None:
|
|
34
|
+
params["to"] = to
|
|
35
|
+
if status is not None:
|
|
36
|
+
params["status"] = status
|
|
37
|
+
if service_type is not None:
|
|
38
|
+
params["serviceType"] = service_type
|
|
39
|
+
if format_param is not None:
|
|
40
|
+
params["format"] = format_param
|
|
41
|
+
if owner_type is not None:
|
|
42
|
+
params["ownerType"] = owner_type
|
|
43
|
+
if storage_region is not None:
|
|
44
|
+
params["storageRegion"] = storage_region
|
|
45
|
+
if location_id is not None:
|
|
46
|
+
params["locationId"] = location_id
|
|
47
|
+
if topic is not None:
|
|
48
|
+
params["topic"] = topic
|
|
49
|
+
if limit > 0:
|
|
50
|
+
params["max"] = limit
|
|
51
|
+
if offset > 0:
|
|
52
|
+
params["start"] = offset
|
|
53
|
+
try:
|
|
54
|
+
if limit > 0:
|
|
55
|
+
result = api.session.rest_get(url, params=params)
|
|
56
|
+
result = result or {}
|
|
57
|
+
items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
|
|
58
|
+
else:
|
|
59
|
+
items = list(api.session.follow_pagination(url=url, params=params, item_key="items"))
|
|
60
|
+
except WebexError as e:
|
|
61
|
+
handle_rest_error(e)
|
|
62
|
+
if output == "json":
|
|
63
|
+
print_json(items)
|
|
64
|
+
else:
|
|
65
|
+
print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("list-converged-recordings")
|
|
70
|
+
def list_converged_recordings(
|
|
71
|
+
from_param: str = typer.Option(None, "--from", help="Starting date and time (inclusive) for recordings to return,"),
|
|
72
|
+
to: str = typer.Option(None, "--to", help="Ending date and time (exclusive) for List recordings to retu"),
|
|
73
|
+
status: str = typer.Option(None, "--status", help="Choices: available, deleted, purged"),
|
|
74
|
+
service_type: str = typer.Option(None, "--service-type", help="Choices: calling, customerAssist"),
|
|
75
|
+
format_param: str = typer.Option(None, "--format", help="Choices: MP3"),
|
|
76
|
+
owner_id: str = typer.Option(None, "--owner-id", help="Webex user Id to fetch recordings for a particular user."),
|
|
77
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Webex email address to fetch recordings for a particular use"),
|
|
78
|
+
owner_type: str = typer.Option(None, "--owner-type", help="Choices: user, place, virtualLine, callQueue"),
|
|
79
|
+
storage_region: str = typer.Option(None, "--storage-region", help="Choices: US, SG, GB, JP, DE, AU, IN, CA"),
|
|
80
|
+
location_id: str = typer.Option(None, "--location-id", help="Fetch recordings for users in a particular Webex Calling loc"),
|
|
81
|
+
topic: str = typer.Option(None, "--topic", help="Recording's topic. If specified, the API filters recordings"),
|
|
82
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
83
|
+
limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
|
|
84
|
+
offset: int = typer.Option(0, "--offset", help="Start offset"),
|
|
85
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
86
|
+
):
|
|
87
|
+
"""List Recordings for Admin or Compliance officer."""
|
|
88
|
+
api = get_api(debug=debug)
|
|
89
|
+
url = f"https://webexapis.com/v1/admin/convergedRecordings"
|
|
90
|
+
params = {}
|
|
91
|
+
if from_param is not None:
|
|
92
|
+
params["from"] = from_param
|
|
93
|
+
if to is not None:
|
|
94
|
+
params["to"] = to
|
|
95
|
+
if status is not None:
|
|
96
|
+
params["status"] = status
|
|
97
|
+
if service_type is not None:
|
|
98
|
+
params["serviceType"] = service_type
|
|
99
|
+
if format_param is not None:
|
|
100
|
+
params["format"] = format_param
|
|
101
|
+
if owner_id is not None:
|
|
102
|
+
params["ownerId"] = owner_id
|
|
103
|
+
if owner_email is not None:
|
|
104
|
+
params["ownerEmail"] = owner_email
|
|
105
|
+
if owner_type is not None:
|
|
106
|
+
params["ownerType"] = owner_type
|
|
107
|
+
if storage_region is not None:
|
|
108
|
+
params["storageRegion"] = storage_region
|
|
109
|
+
if location_id is not None:
|
|
110
|
+
params["locationId"] = location_id
|
|
111
|
+
if topic is not None:
|
|
112
|
+
params["topic"] = topic
|
|
113
|
+
if limit > 0:
|
|
114
|
+
params["max"] = limit
|
|
115
|
+
if offset > 0:
|
|
116
|
+
params["start"] = offset
|
|
117
|
+
try:
|
|
118
|
+
if limit > 0:
|
|
119
|
+
result = api.session.rest_get(url, params=params)
|
|
120
|
+
result = result or {}
|
|
121
|
+
items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
|
|
122
|
+
else:
|
|
123
|
+
items = list(api.session.follow_pagination(url=url, params=params, item_key="items"))
|
|
124
|
+
except WebexError as e:
|
|
125
|
+
handle_rest_error(e)
|
|
126
|
+
if output == "json":
|
|
127
|
+
print_json(items)
|
|
128
|
+
else:
|
|
129
|
+
print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("show")
|
|
134
|
+
def show(
|
|
135
|
+
recording_id: str = typer.Argument(help="recordingId"),
|
|
136
|
+
output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
|
|
137
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
138
|
+
):
|
|
139
|
+
"""Get Recording Details."""
|
|
140
|
+
api = get_api(debug=debug)
|
|
141
|
+
url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}"
|
|
142
|
+
try:
|
|
143
|
+
result = api.session.rest_get(url)
|
|
144
|
+
except WebexError as e:
|
|
145
|
+
handle_rest_error(e)
|
|
146
|
+
if output == "json":
|
|
147
|
+
print_json(result)
|
|
148
|
+
else:
|
|
149
|
+
if isinstance(result, dict):
|
|
150
|
+
print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
|
|
151
|
+
elif isinstance(result, list):
|
|
152
|
+
print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
|
|
153
|
+
else:
|
|
154
|
+
print_json(result)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command("delete")
|
|
159
|
+
def delete(
|
|
160
|
+
recording_id: str = typer.Argument(help="recordingId"),
|
|
161
|
+
force: bool = typer.Option(False, "--force", help="Skip confirmation"),
|
|
162
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
163
|
+
):
|
|
164
|
+
"""Delete a Recording."""
|
|
165
|
+
if not force:
|
|
166
|
+
typer.confirm(f"Delete {recording_id}?", abort=True)
|
|
167
|
+
api = get_api(debug=debug)
|
|
168
|
+
url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}"
|
|
169
|
+
try:
|
|
170
|
+
api.session.rest_delete(url)
|
|
171
|
+
except WebexError as e:
|
|
172
|
+
handle_rest_error(e)
|
|
173
|
+
typer.echo(f"Deleted: {recording_id}")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command("show-metadata")
|
|
178
|
+
def show_metadata(
|
|
179
|
+
recording_id: str = typer.Argument(help="recordingId"),
|
|
180
|
+
show_all_types: str = typer.Option(None, "--show-all-types", help="If `showAllTypes` is `true`, all attributes will be shown. I"),
|
|
181
|
+
output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
|
|
182
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
183
|
+
):
|
|
184
|
+
"""Get Recording metadata."""
|
|
185
|
+
api = get_api(debug=debug)
|
|
186
|
+
url = f"https://webexapis.com/v1/convergedRecordings/{recording_id}/metadata"
|
|
187
|
+
params = {}
|
|
188
|
+
if show_all_types is not None:
|
|
189
|
+
params["showAllTypes"] = show_all_types
|
|
190
|
+
try:
|
|
191
|
+
result = api.session.rest_get(url, params=params)
|
|
192
|
+
except WebexError as e:
|
|
193
|
+
handle_rest_error(e)
|
|
194
|
+
if output == "json":
|
|
195
|
+
print_json(result)
|
|
196
|
+
else:
|
|
197
|
+
if isinstance(result, dict):
|
|
198
|
+
print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
|
|
199
|
+
elif isinstance(result, list):
|
|
200
|
+
print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
|
|
201
|
+
else:
|
|
202
|
+
print_json(result)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("create")
|
|
207
|
+
def create(
|
|
208
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Recording owner email."),
|
|
209
|
+
owner_id: str = typer.Option(None, "--owner-id", help="Recording owner ID. Can be a user, a virtual line, or a work"),
|
|
210
|
+
reassign_owner_email: str = typer.Option(None, "--reassign-owner-email", help="(required) New owner of the recordings."),
|
|
211
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
212
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
213
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
214
|
+
):
|
|
215
|
+
"""Reassign Recordings\n\nExample --json-body:\n '{"reassignOwnerEmail":"...","ownerEmail":"...","ownerID":"...","recordingIds":["..."]}'."""
|
|
216
|
+
api = get_api(debug=debug)
|
|
217
|
+
url = f"https://webexapis.com/v1/convergedRecordings/reassign"
|
|
218
|
+
if json_body:
|
|
219
|
+
body = json.loads(json_body)
|
|
220
|
+
else:
|
|
221
|
+
body = {}
|
|
222
|
+
if owner_email is not None:
|
|
223
|
+
body["ownerEmail"] = owner_email
|
|
224
|
+
if owner_id is not None:
|
|
225
|
+
body["ownerID"] = owner_id
|
|
226
|
+
if reassign_owner_email is not None:
|
|
227
|
+
body["reassignOwnerEmail"] = reassign_owner_email
|
|
228
|
+
_missing = [f for f in ['reassignOwnerEmail'] if f not in body or body[f] is None]
|
|
229
|
+
if _missing:
|
|
230
|
+
typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
try:
|
|
233
|
+
result = api.session.rest_post(url, json=body)
|
|
234
|
+
except WebexError as e:
|
|
235
|
+
handle_rest_error(e)
|
|
236
|
+
if output == "json":
|
|
237
|
+
print_json(result)
|
|
238
|
+
elif isinstance(result, dict) and "id" in result:
|
|
239
|
+
typer.echo(f"Created: {result['id']}")
|
|
240
|
+
elif not result or result == {}:
|
|
241
|
+
typer.echo("Created.")
|
|
242
|
+
else:
|
|
243
|
+
print_json(result)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@app.command("create-soft-delete")
|
|
248
|
+
def create_soft_delete(
|
|
249
|
+
trash_all: bool = typer.Option(None, "--trash-all/--no-trash-all", help="If not specified or `false`, moves the recordings specified"),
|
|
250
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
|
|
251
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
252
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
253
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
254
|
+
):
|
|
255
|
+
"""Move Recordings into the Recycle Bin\n\nExample --json-body:\n '{"trashAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
|
|
256
|
+
api = get_api(debug=debug)
|
|
257
|
+
url = f"https://webexapis.com/v1/convergedRecordings/softDelete"
|
|
258
|
+
if json_body:
|
|
259
|
+
body = json.loads(json_body)
|
|
260
|
+
else:
|
|
261
|
+
body = {}
|
|
262
|
+
if trash_all is not None:
|
|
263
|
+
body["trashAll"] = trash_all
|
|
264
|
+
if owner_email is not None:
|
|
265
|
+
body["ownerEmail"] = owner_email
|
|
266
|
+
try:
|
|
267
|
+
result = api.session.rest_post(url, json=body)
|
|
268
|
+
except WebexError as e:
|
|
269
|
+
handle_rest_error(e)
|
|
270
|
+
if output == "json":
|
|
271
|
+
print_json(result)
|
|
272
|
+
elif isinstance(result, dict) and "id" in result:
|
|
273
|
+
typer.echo(f"Created: {result['id']}")
|
|
274
|
+
elif not result or result == {}:
|
|
275
|
+
typer.echo("Created.")
|
|
276
|
+
else:
|
|
277
|
+
print_json(result)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.command("create-restore")
|
|
282
|
+
def create_restore(
|
|
283
|
+
restore_all: bool = typer.Option(None, "--restore-all/--no-restore-all", help="If not specified or `false`, restores the recordings specifi"),
|
|
284
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
|
|
285
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
286
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
287
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
288
|
+
):
|
|
289
|
+
"""Restore Recordings from Recycle Bin\n\nExample --json-body:\n '{"restoreAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
|
|
290
|
+
api = get_api(debug=debug)
|
|
291
|
+
url = f"https://webexapis.com/v1/convergedRecordings/restore"
|
|
292
|
+
if json_body:
|
|
293
|
+
body = json.loads(json_body)
|
|
294
|
+
else:
|
|
295
|
+
body = {}
|
|
296
|
+
if restore_all is not None:
|
|
297
|
+
body["restoreAll"] = restore_all
|
|
298
|
+
if owner_email is not None:
|
|
299
|
+
body["ownerEmail"] = owner_email
|
|
300
|
+
try:
|
|
301
|
+
result = api.session.rest_post(url, json=body)
|
|
302
|
+
except WebexError as e:
|
|
303
|
+
handle_rest_error(e)
|
|
304
|
+
if output == "json":
|
|
305
|
+
print_json(result)
|
|
306
|
+
elif isinstance(result, dict) and "id" in result:
|
|
307
|
+
typer.echo(f"Created: {result['id']}")
|
|
308
|
+
elif not result or result == {}:
|
|
309
|
+
typer.echo("Created.")
|
|
310
|
+
else:
|
|
311
|
+
print_json(result)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@app.command("create-purge")
|
|
316
|
+
def create_purge(
|
|
317
|
+
purge_all: bool = typer.Option(None, "--purge-all/--no-purge-all", help="If not specified or `false`, purges the recordings specified"),
|
|
318
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Email address for the recording owner. This parameter is onl"),
|
|
319
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
320
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
321
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
322
|
+
):
|
|
323
|
+
"""Purge Recordings from Recycle Bin\n\nExample --json-body:\n '{"purgeAll":true,"ownerEmail":"...","recordingIds":["..."]}'."""
|
|
324
|
+
api = get_api(debug=debug)
|
|
325
|
+
url = f"https://webexapis.com/v1/convergedRecordings/purge"
|
|
326
|
+
if json_body:
|
|
327
|
+
body = json.loads(json_body)
|
|
328
|
+
else:
|
|
329
|
+
body = {}
|
|
330
|
+
if purge_all is not None:
|
|
331
|
+
body["purgeAll"] = purge_all
|
|
332
|
+
if owner_email is not None:
|
|
333
|
+
body["ownerEmail"] = owner_email
|
|
334
|
+
try:
|
|
335
|
+
result = api.session.rest_post(url, json=body)
|
|
336
|
+
except WebexError as e:
|
|
337
|
+
handle_rest_error(e)
|
|
338
|
+
if output == "json":
|
|
339
|
+
print_json(result)
|
|
340
|
+
elif isinstance(result, dict) and "id" in result:
|
|
341
|
+
typer.echo(f"Created: {result['id']}")
|
|
342
|
+
elif not result or result == {}:
|
|
343
|
+
typer.echo("Created.")
|
|
344
|
+
else:
|
|
345
|
+
print_json(result)
|
|
346
|
+
|
|
347
|
+
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Download and bulk-export converged recording artifacts (audio, transcript, AI notes)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from wxcli.errors import WebexError
|
|
13
|
+
|
|
14
|
+
from wxcli.auth import get_api
|
|
15
|
+
from wxcli.errors import handle_rest_error
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
BASE = "https://webexapis.com/v1"
|
|
21
|
+
|
|
22
|
+
# Mapping from temporaryDirectDownloadLinks field → output filename
|
|
23
|
+
ARTIFACT_MAP = {
|
|
24
|
+
"transcriptDownloadLink": ("transcript.txt", "transcript", False),
|
|
25
|
+
"suggestedNotesDownloadLink": ("suggested_notes.html", "suggestedNotes", False),
|
|
26
|
+
"shortNotesDownloadLink": ("short_notes.html", "shortNotes", False),
|
|
27
|
+
"actionItemsDownloadLink": ("action_items.html", "actionItems", False),
|
|
28
|
+
"audioDownloadLink": ("audio.mp3", "audioFile", True),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DownloadResult:
|
|
34
|
+
recording_id: str
|
|
35
|
+
metadata: dict
|
|
36
|
+
downloaded: list[str] = field(default_factory=list)
|
|
37
|
+
unavailable: list[str] = field(default_factory=list)
|
|
38
|
+
failed: list[str] = field(default_factory=list)
|
|
39
|
+
audio_path: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def download_recording_artifacts(
|
|
43
|
+
api,
|
|
44
|
+
recording_id: str,
|
|
45
|
+
output_dir: Path,
|
|
46
|
+
include_audio: bool = False,
|
|
47
|
+
) -> DownloadResult:
|
|
48
|
+
"""Fetch recording detail and download all available artifacts."""
|
|
49
|
+
url = f"{BASE}/convergedRecordings/{recording_id}"
|
|
50
|
+
detail = api.session.rest_get(url)
|
|
51
|
+
|
|
52
|
+
result = DownloadResult(recording_id=recording_id, metadata=detail)
|
|
53
|
+
|
|
54
|
+
# Write metadata.json
|
|
55
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
metadata_path = output_dir / "metadata.json"
|
|
57
|
+
metadata_path.write_text(json.dumps(detail, indent=2, default=str))
|
|
58
|
+
|
|
59
|
+
links = detail.get("temporaryDirectDownloadLinks") or {}
|
|
60
|
+
|
|
61
|
+
for link_field, (filename, _content_key, is_audio) in ARTIFACT_MAP.items():
|
|
62
|
+
if is_audio and not include_audio:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
download_url = links.get(link_field)
|
|
66
|
+
if not download_url:
|
|
67
|
+
result.unavailable.append(filename)
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
if is_audio:
|
|
72
|
+
resp = requests.get(download_url, timeout=300, stream=True)
|
|
73
|
+
resp.raise_for_status()
|
|
74
|
+
else:
|
|
75
|
+
resp = requests.get(download_url, timeout=300)
|
|
76
|
+
resp.raise_for_status()
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning("Failed to download %s for %s: %s", filename, recording_id, e)
|
|
79
|
+
result.failed.append(filename)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
file_path = output_dir / filename
|
|
83
|
+
if is_audio:
|
|
84
|
+
with open(file_path, "wb") as f:
|
|
85
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
86
|
+
f.write(chunk)
|
|
87
|
+
result.audio_path = str(file_path)
|
|
88
|
+
else:
|
|
89
|
+
file_path.write_text(resp.text, encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
result.downloaded.append(filename)
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def register(app: typer.Typer) -> None:
|
|
97
|
+
"""Register download and export commands on the converged-recordings app."""
|
|
98
|
+
|
|
99
|
+
@app.command("download")
|
|
100
|
+
def download(
|
|
101
|
+
recording_id: str = typer.Argument(help="Recording ID to download"),
|
|
102
|
+
include_audio: bool = typer.Option(False, "--include-audio", help="Download the MP3 audio file"),
|
|
103
|
+
output_dir: str = typer.Option("./recordings", "--output-dir", "-d", help="Target directory"),
|
|
104
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
105
|
+
):
|
|
106
|
+
"""Download a single recording's artifacts to a local directory."""
|
|
107
|
+
api = get_api(debug=debug)
|
|
108
|
+
out_path = Path(output_dir) / recording_id
|
|
109
|
+
|
|
110
|
+
console.print(f"Downloading recording {recording_id}...")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
result = download_recording_artifacts(api, recording_id, out_path, include_audio)
|
|
114
|
+
except WebexError as e:
|
|
115
|
+
handle_rest_error(e)
|
|
116
|
+
return # unreachable — handle_rest_error always raises
|
|
117
|
+
except Exception as e:
|
|
118
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
|
|
121
|
+
# Summary
|
|
122
|
+
parts = []
|
|
123
|
+
if result.downloaded:
|
|
124
|
+
parts.append(f"downloaded: {', '.join(result.downloaded)}")
|
|
125
|
+
if result.unavailable:
|
|
126
|
+
parts.append(f"not available: {', '.join(result.unavailable)}")
|
|
127
|
+
if result.failed:
|
|
128
|
+
parts.append(f"failed: {', '.join(result.failed)}")
|
|
129
|
+
|
|
130
|
+
console.print(f" Output: {out_path}")
|
|
131
|
+
console.print(f" {'; '.join(parts)}")
|
|
132
|
+
|
|
133
|
+
if not result.downloaded:
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
|
|
136
|
+
@app.command("export")
|
|
137
|
+
def export(
|
|
138
|
+
from_date: str = typer.Option(..., "--from", help="Start date, ISO 8601 (required)"),
|
|
139
|
+
to_date: str = typer.Option(..., "--to", help="End date, ISO 8601 (required)"),
|
|
140
|
+
owner_email: str = typer.Option(None, "--owner-email", help="Filter by owner email"),
|
|
141
|
+
owner_id: str = typer.Option(None, "--owner-id", help="Filter by owner ID"),
|
|
142
|
+
owner_type: str = typer.Option(None, "--owner-type", help="Filter: user|place|virtualLine|callQueue"),
|
|
143
|
+
location_id: str = typer.Option(None, "--location-id", help="Filter by location ID"),
|
|
144
|
+
service_type: str = typer.Option(None, "--service-type", help="Filter: calling|customerAssist"),
|
|
145
|
+
status: str = typer.Option(None, "--status", help="Filter: available|deleted"),
|
|
146
|
+
topic: str = typer.Option(None, "--topic", help="Filter by topic keyword"),
|
|
147
|
+
include_audio: bool = typer.Option(False, "--include-audio", help="Download MP3 audio files"),
|
|
148
|
+
fmt: str = typer.Option("jsonl", "--format", help="Output format: jsonl|json-per-file"),
|
|
149
|
+
output_dir: str = typer.Option("./recording-export", "--output-dir", "-d", help="Target directory"),
|
|
150
|
+
limit: int = typer.Option(0, "--limit", help="Max recordings to export (0=all)"),
|
|
151
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
152
|
+
):
|
|
153
|
+
"""Bulk export recordings with text/AI artifacts for BI consumption."""
|
|
154
|
+
api = get_api(debug=debug)
|
|
155
|
+
out_path = Path(output_dir)
|
|
156
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
# Build list query params
|
|
159
|
+
params: dict = {"from": from_date, "to": to_date}
|
|
160
|
+
if owner_email:
|
|
161
|
+
params["ownerEmail"] = owner_email
|
|
162
|
+
if owner_id:
|
|
163
|
+
params["ownerId"] = owner_id
|
|
164
|
+
if owner_type:
|
|
165
|
+
params["ownerType"] = owner_type
|
|
166
|
+
if location_id:
|
|
167
|
+
params["locationId"] = location_id
|
|
168
|
+
if service_type:
|
|
169
|
+
params["serviceType"] = service_type
|
|
170
|
+
if status:
|
|
171
|
+
params["status"] = status
|
|
172
|
+
if topic:
|
|
173
|
+
params["topic"] = topic
|
|
174
|
+
|
|
175
|
+
# Paginate the admin listing endpoint
|
|
176
|
+
console.print("Listing recordings...")
|
|
177
|
+
list_url = f"{BASE}/admin/convergedRecordings"
|
|
178
|
+
try:
|
|
179
|
+
recordings = list(api.session.follow_pagination(url=list_url, params=params, item_key="items"))
|
|
180
|
+
if limit > 0:
|
|
181
|
+
recordings = recordings[:limit]
|
|
182
|
+
except WebexError as e:
|
|
183
|
+
handle_rest_error(e)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
if not recordings:
|
|
187
|
+
console.print("[yellow]No recordings found for the given filters.[/yellow]")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
total = len(recordings)
|
|
191
|
+
console.print(f"Found {total} recording(s). Exporting...")
|
|
192
|
+
|
|
193
|
+
# Track stats
|
|
194
|
+
exported = 0
|
|
195
|
+
failed_ids: list[str] = []
|
|
196
|
+
artifact_counts: dict[str, int] = {"downloaded": 0, "unavailable": 0, "failed": 0}
|
|
197
|
+
audio_count = 0
|
|
198
|
+
|
|
199
|
+
# JSONL mode: open the output file
|
|
200
|
+
jsonl_file = None
|
|
201
|
+
if fmt == "jsonl":
|
|
202
|
+
jsonl_file = open(out_path / "recordings.jsonl", "w", encoding="utf-8")
|
|
203
|
+
if include_audio:
|
|
204
|
+
(out_path / "audio").mkdir(exist_ok=True)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
for i, rec in enumerate(recordings, 1):
|
|
208
|
+
rec_id = rec.get("id", "unknown")
|
|
209
|
+
console.print(f"Exporting recording {i}/{total} ({rec_id[:16]}...)...")
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
if fmt == "json-per-file":
|
|
213
|
+
rec_dir = out_path / rec_id
|
|
214
|
+
dl_result = download_recording_artifacts(api, rec_id, rec_dir, include_audio)
|
|
215
|
+
else:
|
|
216
|
+
# JSONL mode: fetch detail, download artifacts inline
|
|
217
|
+
detail_url = f"{BASE}/convergedRecordings/{rec_id}"
|
|
218
|
+
detail = api.session.rest_get(detail_url)
|
|
219
|
+
links = detail.get("temporaryDirectDownloadLinks") or {}
|
|
220
|
+
|
|
221
|
+
# Build the JSONL record — all metadata fields except download links
|
|
222
|
+
record: dict = {
|
|
223
|
+
k: v for k, v in detail.items()
|
|
224
|
+
if k != "temporaryDirectDownloadLinks"
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
dl_result = DownloadResult(recording_id=rec_id, metadata=detail)
|
|
228
|
+
|
|
229
|
+
# Download text artifacts inline
|
|
230
|
+
for link_field, (filename, content_key, is_audio) in ARTIFACT_MAP.items():
|
|
231
|
+
if is_audio:
|
|
232
|
+
continue
|
|
233
|
+
download_url = links.get(link_field)
|
|
234
|
+
if not download_url:
|
|
235
|
+
record[content_key] = None
|
|
236
|
+
dl_result.unavailable.append(filename)
|
|
237
|
+
continue
|
|
238
|
+
try:
|
|
239
|
+
resp = requests.get(download_url, timeout=300)
|
|
240
|
+
resp.raise_for_status()
|
|
241
|
+
record[content_key] = resp.text
|
|
242
|
+
dl_result.downloaded.append(filename)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning("Failed to download %s for %s: %s", filename, rec_id, e)
|
|
245
|
+
record[content_key] = None
|
|
246
|
+
dl_result.failed.append(filename)
|
|
247
|
+
|
|
248
|
+
# Audio (opt-in)
|
|
249
|
+
if include_audio:
|
|
250
|
+
audio_url = links.get("audioDownloadLink")
|
|
251
|
+
if not audio_url:
|
|
252
|
+
record["audioFile"] = None
|
|
253
|
+
dl_result.unavailable.append("audio.mp3")
|
|
254
|
+
else:
|
|
255
|
+
try:
|
|
256
|
+
resp = requests.get(audio_url, timeout=300, stream=True)
|
|
257
|
+
resp.raise_for_status()
|
|
258
|
+
audio_path = out_path / "audio" / f"{rec_id}.mp3"
|
|
259
|
+
with open(audio_path, "wb") as af:
|
|
260
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
261
|
+
af.write(chunk)
|
|
262
|
+
record["audioFile"] = f"audio/{rec_id}.mp3"
|
|
263
|
+
dl_result.audio_path = str(audio_path)
|
|
264
|
+
dl_result.downloaded.append("audio.mp3")
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.warning("Failed to download audio for %s: %s", rec_id, e)
|
|
267
|
+
record["audioFile"] = None
|
|
268
|
+
dl_result.failed.append("audio.mp3")
|
|
269
|
+
else:
|
|
270
|
+
record["audioFile"] = None
|
|
271
|
+
|
|
272
|
+
jsonl_file.write(json.dumps(record, default=str) + "\n")
|
|
273
|
+
jsonl_file.flush()
|
|
274
|
+
|
|
275
|
+
exported += 1
|
|
276
|
+
artifact_counts["downloaded"] += len(dl_result.downloaded)
|
|
277
|
+
artifact_counts["unavailable"] += len(dl_result.unavailable)
|
|
278
|
+
artifact_counts["failed"] += len(dl_result.failed)
|
|
279
|
+
if dl_result.audio_path:
|
|
280
|
+
audio_count += 1
|
|
281
|
+
|
|
282
|
+
except WebexError as e:
|
|
283
|
+
logger.warning("Failed to export recording %s: %s", rec_id, e)
|
|
284
|
+
failed_ids.append(rec_id)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning("Failed to export recording %s: %s", rec_id, e)
|
|
287
|
+
failed_ids.append(rec_id)
|
|
288
|
+
|
|
289
|
+
finally:
|
|
290
|
+
if jsonl_file:
|
|
291
|
+
jsonl_file.close()
|
|
292
|
+
|
|
293
|
+
# Final summary
|
|
294
|
+
console.print("")
|
|
295
|
+
console.print("[bold]Export Summary[/bold]")
|
|
296
|
+
console.print(f" Total recordings: {total}")
|
|
297
|
+
console.print(f" Exported: {exported}")
|
|
298
|
+
console.print(f" Failed: {len(failed_ids)}")
|
|
299
|
+
console.print(f" Artifacts downloaded: {artifact_counts['downloaded']}")
|
|
300
|
+
console.print(f" Artifacts unavailable: {artifact_counts['unavailable']}")
|
|
301
|
+
console.print(f" Artifacts failed: {artifact_counts['failed']}")
|
|
302
|
+
if include_audio:
|
|
303
|
+
console.print(f" Audio files: {audio_count}")
|
|
304
|
+
console.print(f" Output directory: {out_path}")
|
|
305
|
+
|
|
306
|
+
if failed_ids:
|
|
307
|
+
console.print(f"\n[yellow]Failed recording IDs:[/yellow]")
|
|
308
|
+
for fid in failed_ids:
|
|
309
|
+
console.print(f" - {fid}")
|
|
310
|
+
raise typer.Exit(1)
|