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,279 @@
|
|
|
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
|
+
from wxcli.config import get_org_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Manage Webex Calling workspaces.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("list")
|
|
13
|
+
def cmd_list(
|
|
14
|
+
location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Values must originat"),
|
|
15
|
+
workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Location associated with the workspace. Both values from the"),
|
|
16
|
+
floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
|
|
17
|
+
display_name: str = typer.Option(None, "--display-name", help="List workspaces by display name."),
|
|
18
|
+
capacity: str = typer.Option(None, "--capacity", help="List workspaces with the given capacity. Must be -1 or highe"),
|
|
19
|
+
type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
|
|
20
|
+
calling: str = typer.Option(None, "--calling", help="Choices: freeCalling, hybridCalling, webexCalling, webexEdgeForDevices, thirdPartySipCalling, none"),
|
|
21
|
+
supported_devices: str = typer.Option(None, "--supported-devices", help="Choices: collaborationDevices, phones"),
|
|
22
|
+
calendar: str = typer.Option(None, "--calendar", help="Choices: none, google, microsoft"),
|
|
23
|
+
device_hosted_meetings_enabled: str = typer.Option(None, "--device-hosted-meetings-enabled", help="List workspaces enabled for device hosted meetings."),
|
|
24
|
+
device_platform: str = typer.Option(None, "--device-platform", help="Choices: cisco, microsoftTeamsRoom"),
|
|
25
|
+
health_level: str = typer.Option(None, "--health-level", help="Choices: error, warning, info, ok"),
|
|
26
|
+
include_devices: str = typer.Option(None, "--include-devices", help="Flag identifying whether to include the devices associated w"),
|
|
27
|
+
include_capabilities: str = typer.Option(None, "--include-capabilities", help="Flag identifying whether to include the workspace capabiliti"),
|
|
28
|
+
planned_maintenance: str = typer.Option(None, "--planned-maintenance", help="Choices: off, on, upcoming"),
|
|
29
|
+
custom_attribute: str = typer.Option(None, "--custom-attribute", help="List workspaces with given custom attribute key."),
|
|
30
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
31
|
+
limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
|
|
32
|
+
offset: int = typer.Option(0, "--offset", help="Start offset"),
|
|
33
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
34
|
+
):
|
|
35
|
+
"""List Workspaces."""
|
|
36
|
+
api = get_api(debug=debug)
|
|
37
|
+
url = f"https://webexapis.com/v1/workspaces"
|
|
38
|
+
params = {}
|
|
39
|
+
if location_id is not None:
|
|
40
|
+
params["locationId"] = location_id
|
|
41
|
+
if workspace_location_id is not None:
|
|
42
|
+
params["workspaceLocationId"] = workspace_location_id
|
|
43
|
+
if floor_id is not None:
|
|
44
|
+
params["floorId"] = floor_id
|
|
45
|
+
if display_name is not None:
|
|
46
|
+
params["displayName"] = display_name
|
|
47
|
+
if capacity is not None:
|
|
48
|
+
params["capacity"] = capacity
|
|
49
|
+
if type_param is not None:
|
|
50
|
+
params["type"] = type_param
|
|
51
|
+
if calling is not None:
|
|
52
|
+
params["calling"] = calling
|
|
53
|
+
if supported_devices is not None:
|
|
54
|
+
params["supportedDevices"] = supported_devices
|
|
55
|
+
if calendar is not None:
|
|
56
|
+
params["calendar"] = calendar
|
|
57
|
+
if device_hosted_meetings_enabled is not None:
|
|
58
|
+
params["deviceHostedMeetingsEnabled"] = device_hosted_meetings_enabled
|
|
59
|
+
if device_platform is not None:
|
|
60
|
+
params["devicePlatform"] = device_platform
|
|
61
|
+
if health_level is not None:
|
|
62
|
+
params["healthLevel"] = health_level
|
|
63
|
+
if include_devices is not None:
|
|
64
|
+
params["includeDevices"] = include_devices
|
|
65
|
+
if include_capabilities is not None:
|
|
66
|
+
params["includeCapabilities"] = include_capabilities
|
|
67
|
+
if planned_maintenance is not None:
|
|
68
|
+
params["plannedMaintenance"] = planned_maintenance
|
|
69
|
+
if custom_attribute is not None:
|
|
70
|
+
params["customAttribute"] = custom_attribute
|
|
71
|
+
if limit > 0:
|
|
72
|
+
params["max"] = limit
|
|
73
|
+
if offset > 0:
|
|
74
|
+
params["start"] = offset
|
|
75
|
+
org_id = get_org_id()
|
|
76
|
+
if org_id is not None:
|
|
77
|
+
params["orgId"] = org_id
|
|
78
|
+
result = None
|
|
79
|
+
try:
|
|
80
|
+
result = api.session.rest_get(url, params=params)
|
|
81
|
+
except WebexError as e:
|
|
82
|
+
handle_rest_error(e)
|
|
83
|
+
result = result or []
|
|
84
|
+
items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
|
|
85
|
+
if output == "json":
|
|
86
|
+
print_json(items)
|
|
87
|
+
else:
|
|
88
|
+
print_table(items, columns=[('ID', 'id'), ('Display Name', 'displayName'), ('Type', 'type'), ('Calling', 'calling.type')], limit=limit)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.command("create")
|
|
93
|
+
def create(
|
|
94
|
+
display_name: str = typer.Option(None, "--display-name", help="(required) A friendly name for the workspace."),
|
|
95
|
+
org_id: str = typer.Option(None, "--org-id", help="`OrgId` associated with the workspace. Only admin users of a"),
|
|
96
|
+
location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Must be provided whe"),
|
|
97
|
+
workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Legacy workspace location ID associated with the workspace."),
|
|
98
|
+
floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
|
|
99
|
+
capacity: str = typer.Option(None, "--capacity", help="How many people the workspace is suitable for. If set, must"),
|
|
100
|
+
type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
|
|
101
|
+
sip_address: str = typer.Option(None, "--sip-address", help="The `sipAddress` field can only be provided when calling typ"),
|
|
102
|
+
notes: str = typer.Option(None, "--notes", help="Notes associated to the workspace."),
|
|
103
|
+
hotdesking_status: str = typer.Option(None, "--hotdesking-status", help="Choices: on, off"),
|
|
104
|
+
supported_devices: str = typer.Option(None, "--supported-devices", help="Choices: collaborationDevices, phones"),
|
|
105
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
106
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
107
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
108
|
+
):
|
|
109
|
+
"""Create a Workspace\n\nExample --json-body:\n '{"displayName":"...","orgId":"...","locationId":"...","workspaceLocationId":"...","floorId":"...","capacity":0,"type":"notSet","sipAddress":"..."}'."""
|
|
110
|
+
api = get_api(debug=debug)
|
|
111
|
+
url = f"https://webexapis.com/v1/workspaces"
|
|
112
|
+
if json_body:
|
|
113
|
+
body = json.loads(json_body)
|
|
114
|
+
else:
|
|
115
|
+
body = {}
|
|
116
|
+
if display_name is not None:
|
|
117
|
+
body["displayName"] = display_name
|
|
118
|
+
if org_id is not None:
|
|
119
|
+
body["orgId"] = org_id
|
|
120
|
+
if location_id is not None:
|
|
121
|
+
body["locationId"] = location_id
|
|
122
|
+
if workspace_location_id is not None:
|
|
123
|
+
body["workspaceLocationId"] = workspace_location_id
|
|
124
|
+
if floor_id is not None:
|
|
125
|
+
body["floorId"] = floor_id
|
|
126
|
+
if capacity is not None:
|
|
127
|
+
body["capacity"] = capacity
|
|
128
|
+
if type_param is not None:
|
|
129
|
+
body["type"] = type_param
|
|
130
|
+
if sip_address is not None:
|
|
131
|
+
body["sipAddress"] = sip_address
|
|
132
|
+
if notes is not None:
|
|
133
|
+
body["notes"] = notes
|
|
134
|
+
if hotdesking_status is not None:
|
|
135
|
+
body["hotdeskingStatus"] = hotdesking_status
|
|
136
|
+
if supported_devices is not None:
|
|
137
|
+
body["supportedDevices"] = supported_devices
|
|
138
|
+
_missing = [f for f in ['displayName'] if f not in body or body[f] is None]
|
|
139
|
+
if _missing:
|
|
140
|
+
typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
|
|
141
|
+
raise typer.Exit(1)
|
|
142
|
+
try:
|
|
143
|
+
result = api.session.rest_post(url, json=body)
|
|
144
|
+
except WebexError as e:
|
|
145
|
+
handle_rest_error(e)
|
|
146
|
+
if output == "json":
|
|
147
|
+
print_json(result)
|
|
148
|
+
elif isinstance(result, dict) and "id" in result:
|
|
149
|
+
typer.echo(f"Created: {result['id']}")
|
|
150
|
+
elif not result or result == {}:
|
|
151
|
+
typer.echo("Created.")
|
|
152
|
+
else:
|
|
153
|
+
print_json(result)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.command("show")
|
|
158
|
+
def show(
|
|
159
|
+
workspace_id: str = typer.Argument(help="workspaceId"),
|
|
160
|
+
include_devices: str = typer.Option(None, "--include-devices", help="Flag identifying whether to include the devices associated w"),
|
|
161
|
+
include_capabilities: str = typer.Option(None, "--include-capabilities", help="Flag identifying whether to include the workspace capabiliti"),
|
|
162
|
+
output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
|
|
163
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
164
|
+
):
|
|
165
|
+
"""Get Workspace Details."""
|
|
166
|
+
api = get_api(debug=debug)
|
|
167
|
+
url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
|
|
168
|
+
params = {}
|
|
169
|
+
if include_devices is not None:
|
|
170
|
+
params["includeDevices"] = include_devices
|
|
171
|
+
if include_capabilities is not None:
|
|
172
|
+
params["includeCapabilities"] = include_capabilities
|
|
173
|
+
try:
|
|
174
|
+
result = api.session.rest_get(url, params=params)
|
|
175
|
+
except WebexError as e:
|
|
176
|
+
handle_rest_error(e)
|
|
177
|
+
if output == "json":
|
|
178
|
+
print_json(result)
|
|
179
|
+
else:
|
|
180
|
+
if isinstance(result, dict):
|
|
181
|
+
print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
|
|
182
|
+
elif isinstance(result, list):
|
|
183
|
+
print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
|
|
184
|
+
else:
|
|
185
|
+
print_json(result)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command("update")
|
|
190
|
+
def update(
|
|
191
|
+
workspace_id: str = typer.Argument(help="workspaceId"),
|
|
192
|
+
display_name: str = typer.Option(None, "--display-name", help="A friendly name for the workspace."),
|
|
193
|
+
location_id: str = typer.Option(None, "--location-id", help="Location associated with the workspace. Must be provided whe"),
|
|
194
|
+
workspace_location_id: str = typer.Option(None, "--workspace-location-id", help="Legacy workspace location ID associated with the workspace."),
|
|
195
|
+
floor_id: str = typer.Option(None, "--floor-id", help="Floor associated with the workspace."),
|
|
196
|
+
capacity: str = typer.Option(None, "--capacity", help="How many people the workspace is suitable for. If set, must"),
|
|
197
|
+
type_param: str = typer.Option(None, "--type", help="Choices: notSet, focus, huddle, meetingRoom, open, desk, other"),
|
|
198
|
+
sip_address: str = typer.Option(None, "--sip-address", help="The `sipAddress` field can only be provided when calling typ"),
|
|
199
|
+
notes: str = typer.Option(None, "--notes", help="Notes associated to the workspace."),
|
|
200
|
+
hotdesking_status: str = typer.Option(None, "--hotdesking-status", help="Choices: on, off"),
|
|
201
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
202
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
203
|
+
):
|
|
204
|
+
"""Update a Workspace\n\nExample --json-body:\n '{"displayName":"...","locationId":"...","workspaceLocationId":"...","floorId":"...","capacity":0,"type":"notSet","calendar":{"type":"...","emailAddress":"...","resourceGroupId":"..."},"sipAddress":"..."}'."""
|
|
205
|
+
api = get_api(debug=debug)
|
|
206
|
+
url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
|
|
207
|
+
if json_body:
|
|
208
|
+
body = json.loads(json_body)
|
|
209
|
+
else:
|
|
210
|
+
body = {}
|
|
211
|
+
if display_name is not None:
|
|
212
|
+
body["displayName"] = display_name
|
|
213
|
+
if location_id is not None:
|
|
214
|
+
body["locationId"] = location_id
|
|
215
|
+
if workspace_location_id is not None:
|
|
216
|
+
body["workspaceLocationId"] = workspace_location_id
|
|
217
|
+
if floor_id is not None:
|
|
218
|
+
body["floorId"] = floor_id
|
|
219
|
+
if capacity is not None:
|
|
220
|
+
body["capacity"] = capacity
|
|
221
|
+
if type_param is not None:
|
|
222
|
+
body["type"] = type_param
|
|
223
|
+
if sip_address is not None:
|
|
224
|
+
body["sipAddress"] = sip_address
|
|
225
|
+
if notes is not None:
|
|
226
|
+
body["notes"] = notes
|
|
227
|
+
if hotdesking_status is not None:
|
|
228
|
+
body["hotdeskingStatus"] = hotdesking_status
|
|
229
|
+
try:
|
|
230
|
+
result = api.session.rest_put(url, json=body)
|
|
231
|
+
except WebexError as e:
|
|
232
|
+
handle_rest_error(e)
|
|
233
|
+
typer.echo(f"Updated.")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.command("delete")
|
|
238
|
+
def delete(
|
|
239
|
+
workspace_id: str = typer.Argument(help="workspaceId"),
|
|
240
|
+
force: bool = typer.Option(False, "--force", help="Skip confirmation"),
|
|
241
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
242
|
+
):
|
|
243
|
+
"""Delete a Workspace."""
|
|
244
|
+
if not force:
|
|
245
|
+
typer.confirm(f"Delete {workspace_id}?", abort=True)
|
|
246
|
+
api = get_api(debug=debug)
|
|
247
|
+
url = f"https://webexapis.com/v1/workspaces/{workspace_id}"
|
|
248
|
+
try:
|
|
249
|
+
api.session.rest_delete(url)
|
|
250
|
+
except WebexError as e:
|
|
251
|
+
handle_rest_error(e)
|
|
252
|
+
typer.echo(f"Deleted: {workspace_id}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@app.command("show-capabilities")
|
|
257
|
+
def show_capabilities(
|
|
258
|
+
workspace_id: str = typer.Argument(help="workspaceId"),
|
|
259
|
+
output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
|
|
260
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
261
|
+
):
|
|
262
|
+
"""Get Workspace Capabilities."""
|
|
263
|
+
api = get_api(debug=debug)
|
|
264
|
+
url = f"https://webexapis.com/v1/workspaces/{workspace_id}/capabilities"
|
|
265
|
+
try:
|
|
266
|
+
result = api.session.rest_get(url)
|
|
267
|
+
except WebexError as e:
|
|
268
|
+
handle_rest_error(e)
|
|
269
|
+
if output == "json":
|
|
270
|
+
print_json(result)
|
|
271
|
+
else:
|
|
272
|
+
if isinstance(result, dict):
|
|
273
|
+
print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
|
|
274
|
+
elif isinstance(result, list):
|
|
275
|
+
print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
|
|
276
|
+
else:
|
|
277
|
+
print_json(result)
|
|
278
|
+
|
|
279
|
+
|
wxcli/commands/xapi.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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 xapi.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command("show")
|
|
12
|
+
def show(
|
|
13
|
+
device_id: str = typer.Option(..., "--device-id", help="The unique identifier for the Webex RoomOS Device."),
|
|
14
|
+
name: str = typer.Option(..., "--name", help="A list of status expressions used to query the Webex RoomOS"),
|
|
15
|
+
output: str = typer.Option("json", "--output", "-o", help="Output format: table|json"),
|
|
16
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
17
|
+
):
|
|
18
|
+
"""Query Status."""
|
|
19
|
+
api = get_api(debug=debug)
|
|
20
|
+
url = f"https://webexapis.com/v1/xapi/status"
|
|
21
|
+
params = {}
|
|
22
|
+
if device_id is not None:
|
|
23
|
+
params["deviceId"] = device_id
|
|
24
|
+
if name is not None:
|
|
25
|
+
params["name"] = name
|
|
26
|
+
try:
|
|
27
|
+
result = api.session.rest_get(url, params=params)
|
|
28
|
+
except WebexError as e:
|
|
29
|
+
handle_rest_error(e)
|
|
30
|
+
if output == "json":
|
|
31
|
+
print_json(result)
|
|
32
|
+
else:
|
|
33
|
+
if isinstance(result, dict):
|
|
34
|
+
print_table([result], columns=[("Key", ""), ("Value", "")], limit=0)
|
|
35
|
+
elif isinstance(result, list):
|
|
36
|
+
print_table(result, columns=[("ID", "id"), ("Name", "name")], limit=0)
|
|
37
|
+
else:
|
|
38
|
+
print_json(result)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("list")
|
|
43
|
+
def cmd_list(
|
|
44
|
+
device_id: str = typer.Option(..., "--device-id", help="A list of device IDs to query schemas from. A request can co"),
|
|
45
|
+
status: str = typer.Option(None, "--status", help="A list of status key expressions to query schemas for. Suppo"),
|
|
46
|
+
command: str = typer.Option(None, "--command", help="A list of command key expressions to query schemas for. Supp"),
|
|
47
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table|json"),
|
|
48
|
+
limit: int = typer.Option(0, "--limit", help="Max results (0=all for paginated endpoints, API default for non-paginated)"),
|
|
49
|
+
offset: int = typer.Option(0, "--offset", help="Start offset"),
|
|
50
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
51
|
+
):
|
|
52
|
+
"""Query Schema."""
|
|
53
|
+
api = get_api(debug=debug)
|
|
54
|
+
url = f"https://webexapis.com/v1/xapi/schema"
|
|
55
|
+
params = {}
|
|
56
|
+
if device_id is not None:
|
|
57
|
+
params["deviceId"] = device_id
|
|
58
|
+
if status is not None:
|
|
59
|
+
params["status"] = status
|
|
60
|
+
if command is not None:
|
|
61
|
+
params["command"] = command
|
|
62
|
+
if limit > 0:
|
|
63
|
+
params["max"] = limit
|
|
64
|
+
if offset > 0:
|
|
65
|
+
params["start"] = offset
|
|
66
|
+
result = None
|
|
67
|
+
try:
|
|
68
|
+
result = api.session.rest_get(url, params=params)
|
|
69
|
+
except WebexError as e:
|
|
70
|
+
handle_rest_error(e)
|
|
71
|
+
result = result or []
|
|
72
|
+
items = result.get("items", result.get("data", result if isinstance(result, list) else [])) if isinstance(result, dict) else (result if isinstance(result, list) else [])
|
|
73
|
+
if output == "json":
|
|
74
|
+
print_json(items)
|
|
75
|
+
else:
|
|
76
|
+
print_table(items, columns=[("ID", "id"), ("Name", "name")], limit=limit)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@app.command("create")
|
|
81
|
+
def create(
|
|
82
|
+
command_name: str = typer.Argument(help="commandName"),
|
|
83
|
+
device_id: str = typer.Option(None, "--device-id", help="(required) The unique identifier for the Webex RoomOS Device."),
|
|
84
|
+
json_body: str = typer.Option(None, "--json-body", help="Full JSON body (overrides other options)"),
|
|
85
|
+
output: str = typer.Option("id", "--output", "-o", help="Output format: id|json"),
|
|
86
|
+
debug: bool = typer.Option(False, "--debug"),
|
|
87
|
+
):
|
|
88
|
+
"""Execute Command\n\nExample --json-body:\n '{"deviceId":"...","arguments":{"Level":0},"body":{"Booking":{"Id":"...","Title":"...","Protocol":"...","Time":"...","Organizer":"...","Number":"..."}}}'."""
|
|
89
|
+
api = get_api(debug=debug)
|
|
90
|
+
url = f"https://webexapis.com/v1/xapi/command/{command_name}"
|
|
91
|
+
if json_body:
|
|
92
|
+
body = json.loads(json_body)
|
|
93
|
+
else:
|
|
94
|
+
body = {}
|
|
95
|
+
if device_id is not None:
|
|
96
|
+
body["deviceId"] = device_id
|
|
97
|
+
_missing = [f for f in ['deviceId'] if f not in body or body[f] is None]
|
|
98
|
+
if _missing:
|
|
99
|
+
typer.echo("Error: Missing required fields: " + ", ".join(_missing), err=True)
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
try:
|
|
102
|
+
result = api.session.rest_post(url, json=body)
|
|
103
|
+
except WebexError as e:
|
|
104
|
+
handle_rest_error(e)
|
|
105
|
+
if output == "json":
|
|
106
|
+
print_json(result)
|
|
107
|
+
elif isinstance(result, dict) and "id" in result:
|
|
108
|
+
typer.echo(f"Created: {result['id']}")
|
|
109
|
+
elif not result or result == {}:
|
|
110
|
+
typer.echo("Created.")
|
|
111
|
+
else:
|
|
112
|
+
print_json(result)
|
|
113
|
+
|
|
114
|
+
|
wxcli/config.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".wxcli" / "config.json"
|
|
6
|
+
|
|
7
|
+
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> dict:
|
|
8
|
+
if not path.exists():
|
|
9
|
+
return {"profiles": {}}
|
|
10
|
+
with open(path, encoding="utf-8") as f:
|
|
11
|
+
return json.load(f)
|
|
12
|
+
|
|
13
|
+
def save_config(data: dict, path: Path = DEFAULT_CONFIG_PATH) -> None:
|
|
14
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
16
|
+
json.dump(data, f, indent=2)
|
|
17
|
+
|
|
18
|
+
def get_token(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
|
|
19
|
+
config = load_config(path)
|
|
20
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
21
|
+
return profile.get("token")
|
|
22
|
+
|
|
23
|
+
def get_expires_at(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
|
|
24
|
+
config = load_config(path)
|
|
25
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
26
|
+
return profile.get("expires_at")
|
|
27
|
+
|
|
28
|
+
def get_org_id(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
|
|
29
|
+
config = load_config(path)
|
|
30
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
31
|
+
return profile.get("org_id")
|
|
32
|
+
|
|
33
|
+
def get_org_name(path: Path = DEFAULT_CONFIG_PATH) -> str | None:
|
|
34
|
+
config = load_config(path)
|
|
35
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
36
|
+
return profile.get("org_name")
|
|
37
|
+
|
|
38
|
+
def decode_webex_id(encoded_id: str) -> str:
|
|
39
|
+
"""Decode a base64-encoded Webex Spark ID to its UUID suffix.
|
|
40
|
+
|
|
41
|
+
Webex Calling APIs return orgId as a base64 URN like:
|
|
42
|
+
Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9iODQxMDE0Ny02MTA0LTQyZTgt...
|
|
43
|
+
which decodes to: ciscospark://us/ORGANIZATION/b8410147-6104-42e8-9b93-...
|
|
44
|
+
|
|
45
|
+
The Contact Center API requires the bare UUID (after the last '/'),
|
|
46
|
+
not the full base64 string.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
padding = 4 - len(encoded_id) % 4
|
|
50
|
+
if padding != 4:
|
|
51
|
+
encoded_id += "=" * padding
|
|
52
|
+
decoded = base64.b64decode(encoded_id).decode("utf-8")
|
|
53
|
+
return decoded.rsplit("/", 1)[-1]
|
|
54
|
+
except Exception:
|
|
55
|
+
return encoded_id
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_org_id(api_session) -> str:
|
|
59
|
+
"""Return the org ID from config, or fetch it from /v1/people/me."""
|
|
60
|
+
saved = get_org_id()
|
|
61
|
+
if saved:
|
|
62
|
+
return saved
|
|
63
|
+
return api_session.rest_get("https://webexapis.com/v1/people/me").get("orgId", "")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_cc_org_id(api_session) -> str:
|
|
67
|
+
"""Return the decoded UUID org ID suitable for CC API path parameters.
|
|
68
|
+
|
|
69
|
+
Uses the saved org_id from config if available (already decoded), otherwise
|
|
70
|
+
fetches from people/me and decodes the base64 Spark ID.
|
|
71
|
+
"""
|
|
72
|
+
saved = get_org_id()
|
|
73
|
+
if saved:
|
|
74
|
+
return decode_webex_id(saved)
|
|
75
|
+
raw = api_session.rest_get("https://webexapis.com/v1/people/me").get("orgId", "")
|
|
76
|
+
return decode_webex_id(raw)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
CC_REGIONS = {
|
|
80
|
+
"us1": "https://api.wxcc-us1.cisco.com",
|
|
81
|
+
"eu1": "https://api.wxcc-eu1.cisco.com",
|
|
82
|
+
"eu2": "https://api.wxcc-eu2.cisco.com",
|
|
83
|
+
"anz1": "https://api.wxcc-anz1.cisco.com",
|
|
84
|
+
"ca1": "https://api.wxcc-ca1.cisco.com",
|
|
85
|
+
"jp1": "https://api.wxcc-jp1.cisco.com",
|
|
86
|
+
"sg1": "https://api.wxcc-sg1.cisco.com",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def get_cc_base_url(path: Path = DEFAULT_CONFIG_PATH) -> str:
|
|
90
|
+
config = load_config(path)
|
|
91
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
92
|
+
region = profile.get("cc_region", "us1")
|
|
93
|
+
return CC_REGIONS.get(region, CC_REGIONS["us1"])
|
|
94
|
+
|
|
95
|
+
def save_cc_region(region: str, path: Path = DEFAULT_CONFIG_PATH) -> None:
|
|
96
|
+
config = load_config(path)
|
|
97
|
+
profile = config.setdefault("profiles", {}).setdefault("default", {})
|
|
98
|
+
profile["cc_region"] = region
|
|
99
|
+
save_config(config, path)
|
|
100
|
+
|
|
101
|
+
FS_DATACENTERS = {
|
|
102
|
+
"intgus1": "https://flow-store.intgus1.ciscoccservice.com/flow-store",
|
|
103
|
+
"us1": "https://flow-store.us1.ciscoccservice.com/flow-store",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
def get_fs_base_url(path: Path = DEFAULT_CONFIG_PATH) -> str:
|
|
107
|
+
config = load_config(path)
|
|
108
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
109
|
+
dc = profile.get("fs_datacenter", "intgus1")
|
|
110
|
+
return FS_DATACENTERS.get(dc, FS_DATACENTERS["intgus1"])
|
|
111
|
+
|
|
112
|
+
def get_fs_project_id(path: Path = DEFAULT_CONFIG_PATH) -> str:
|
|
113
|
+
config = load_config(path)
|
|
114
|
+
profile = config.get("profiles", {}).get("default", {})
|
|
115
|
+
pid = profile.get("fs_project_id")
|
|
116
|
+
if pid:
|
|
117
|
+
return pid
|
|
118
|
+
raise SystemExit(
|
|
119
|
+
"Flow Store project ID not configured. Run:\n"
|
|
120
|
+
" wxcli fs-projects list # to find your project ID\n"
|
|
121
|
+
" Then set it in ~/.wxcli/config.json under profiles.default.fs_project_id"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def save_fs_datacenter(datacenter: str, path: Path = DEFAULT_CONFIG_PATH) -> None:
|
|
125
|
+
config = load_config(path)
|
|
126
|
+
profile = config.setdefault("profiles", {}).setdefault("default", {})
|
|
127
|
+
profile["fs_datacenter"] = datacenter
|
|
128
|
+
save_config(config, path)
|
|
129
|
+
|
|
130
|
+
def save_org(org_id: str | None, org_name: str | None, path: Path = DEFAULT_CONFIG_PATH) -> None:
|
|
131
|
+
config = load_config(path)
|
|
132
|
+
profile = config.setdefault("profiles", {}).setdefault("default", {})
|
|
133
|
+
if org_id:
|
|
134
|
+
profile["org_id"] = org_id
|
|
135
|
+
profile["org_name"] = org_name
|
|
136
|
+
else:
|
|
137
|
+
profile.pop("org_id", None)
|
|
138
|
+
profile.pop("org_name", None)
|
|
139
|
+
save_config(config, path)
|
wxcli/errors.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Centralized error handler for wxcli commands."""
|
|
2
|
+
import json
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WebexError(Exception):
|
|
7
|
+
"""Webex API error. Message-first: WebexError(response.text) remains valid.
|
|
8
|
+
|
|
9
|
+
status_code and body (parsed JSON error, when parseable) are optional
|
|
10
|
+
enrichments so handlers can key off status instead of substring matching.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, status_code: int | None = None, body: dict | None = None):
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.body = body
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_ERROR_TIPS = {
|
|
20
|
+
4003: "This endpoint requires a user-level OAuth token, not an admin or service app token.",
|
|
21
|
+
4008: "This endpoint requires the target user to have a Webex Calling license.",
|
|
22
|
+
9601: "This endpoint requires a user-level OAuth token, not an admin or service app token.",
|
|
23
|
+
25008: "Use --json-body for full control over the request body.",
|
|
24
|
+
25409: "This workspace setting requires a Professional license. Use -o json with the /features/ path commands for Basic workspaces.",
|
|
25
|
+
28018: "CX Essentials is not enabled for this queue. Use --has-cx-essentials true when creating/querying CX queues.",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_MESSAGE_TIPS = {
|
|
29
|
+
"Target user not authorized": "This endpoint requires a user-level OAuth token, not an admin or service app token.",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_error_code(err: str) -> int | None:
|
|
34
|
+
"""Extract error code from a Webex API JSON error response."""
|
|
35
|
+
try:
|
|
36
|
+
body = json.loads(err)
|
|
37
|
+
for entry in body.get("errors", []):
|
|
38
|
+
code = entry.get("errorCode") or entry.get("code")
|
|
39
|
+
if code is not None:
|
|
40
|
+
return int(code)
|
|
41
|
+
except (json.JSONDecodeError, ValueError, AttributeError):
|
|
42
|
+
pass
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _truncate_html(err: str) -> str:
|
|
47
|
+
"""Extract a readable message from HTML error responses."""
|
|
48
|
+
if not err.lstrip().startswith("<"):
|
|
49
|
+
return err
|
|
50
|
+
import re
|
|
51
|
+
title = re.search(r"<title>(.*?)</title>", err, re.IGNORECASE)
|
|
52
|
+
h1 = re.search(r"<h1>(.*?)</h1>", err, re.IGNORECASE)
|
|
53
|
+
msg = (h1 and h1.group(1)) or (title and title.group(1)) or "Non-JSON error response"
|
|
54
|
+
return msg.strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def handle_rest_error(e: WebexError) -> None:
|
|
58
|
+
"""Centralized error handler with actionable tips."""
|
|
59
|
+
err = str(e)
|
|
60
|
+
code = _extract_error_code(err)
|
|
61
|
+
|
|
62
|
+
if code == 4003 and "Target user not authorized" not in err:
|
|
63
|
+
code = None
|
|
64
|
+
|
|
65
|
+
if code and code in _ERROR_TIPS:
|
|
66
|
+
typer.echo(f"Error: {_truncate_html(err)}", err=True)
|
|
67
|
+
typer.echo(f"Tip: {_ERROR_TIPS[code]}", err=True)
|
|
68
|
+
elif any(msg in err for msg in _MESSAGE_TIPS):
|
|
69
|
+
tip = next(v for k, v in _MESSAGE_TIPS.items() if k in err)
|
|
70
|
+
typer.echo(f"Error: {_truncate_html(err)}", err=True)
|
|
71
|
+
typer.echo(f"Tip: {tip}", err=True)
|
|
72
|
+
elif "wxcc" in err and "403" in err:
|
|
73
|
+
typer.echo(f"Error: {_truncate_html(err)}", err=True)
|
|
74
|
+
typer.echo("Tip: Contact Center APIs require CC-scoped OAuth (cjp:config_read / cjp:config_write). Standard admin tokens won't work.", err=True)
|
|
75
|
+
else:
|
|
76
|
+
typer.echo(f"Error: {_truncate_html(err)}", err=True)
|
|
77
|
+
raise typer.Exit(1)
|