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,571 @@
|
|
|
1
|
+
"""Runtime execution support — operation metadata, status tracking, rollback.
|
|
2
|
+
|
|
3
|
+
Returns operation metadata (resource type, canonical data, resolved
|
|
4
|
+
dependency IDs) instead of pre-built CLI command strings. The cucm-migrate
|
|
5
|
+
skill uses this metadata to delegate to domain skills, which build and
|
|
6
|
+
execute the actual commands.
|
|
7
|
+
|
|
8
|
+
(Phase 12b — runtime.py)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from wxcli.migration.store import MigrationStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _now() -> str:
|
|
22
|
+
return datetime.now(timezone.utc).isoformat()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_next_batch(store: MigrationStore) -> list[dict[str, Any]]:
|
|
26
|
+
"""Return all pending ops whose dependencies are all completed.
|
|
27
|
+
|
|
28
|
+
Algorithm:
|
|
29
|
+
1. Query plan_operations WHERE status = 'pending'
|
|
30
|
+
2. For each pending op, check plan_edges: all predecessors must be
|
|
31
|
+
status = 'completed' or 'skipped'
|
|
32
|
+
3. Group qualifying ops by (batch, tier), return the lowest group
|
|
33
|
+
4. For each op, include the canonical object data and resolved
|
|
34
|
+
dependency IDs (webex_ids from completed predecessor ops)
|
|
35
|
+
|
|
36
|
+
Returns list of dicts:
|
|
37
|
+
{
|
|
38
|
+
"node_id": str,
|
|
39
|
+
"resource_type": str,
|
|
40
|
+
"op_type": str,
|
|
41
|
+
"canonical_id": str,
|
|
42
|
+
"description": str,
|
|
43
|
+
"batch": str | None,
|
|
44
|
+
"tier": int,
|
|
45
|
+
"data": dict,
|
|
46
|
+
"resolved_deps": dict,
|
|
47
|
+
}
|
|
48
|
+
"""
|
|
49
|
+
conn = store.conn
|
|
50
|
+
|
|
51
|
+
# Find pending ops whose HARD dependencies are all completed or skipped.
|
|
52
|
+
# SOFT deps (e.g., agent membership in hunt groups) do NOT block —
|
|
53
|
+
# a failed SOFT dep means the feature proceeds without that agent.
|
|
54
|
+
# Only REQUIRES and CONFIGURES edges block execution.
|
|
55
|
+
ready_rows = conn.execute(
|
|
56
|
+
"""SELECT po.node_id, po.canonical_id, po.op_type, po.resource_type,
|
|
57
|
+
po.tier, po.batch, po.description, po.data_json
|
|
58
|
+
FROM plan_operations po
|
|
59
|
+
WHERE po.status = 'pending'
|
|
60
|
+
AND NOT EXISTS (
|
|
61
|
+
SELECT 1 FROM plan_edges pe
|
|
62
|
+
JOIN plan_operations dep ON dep.node_id = pe.from_node
|
|
63
|
+
WHERE pe.to_node = po.node_id
|
|
64
|
+
AND pe.broken = 0
|
|
65
|
+
AND pe.dep_type != 'soft'
|
|
66
|
+
AND dep.status NOT IN ('completed', 'skipped')
|
|
67
|
+
)
|
|
68
|
+
ORDER BY po.tier, po.batch"""
|
|
69
|
+
).fetchall()
|
|
70
|
+
|
|
71
|
+
if not ready_rows:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
# Group by (tier, batch) and return the lowest group
|
|
75
|
+
first_tier = ready_rows[0]["tier"]
|
|
76
|
+
first_batch = ready_rows[0]["batch"]
|
|
77
|
+
batch_ops = [
|
|
78
|
+
r for r in ready_rows
|
|
79
|
+
if r["tier"] == first_tier and r["batch"] == first_batch
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
result = []
|
|
83
|
+
for row in batch_ops:
|
|
84
|
+
node_id = row["node_id"]
|
|
85
|
+
canonical_id = row["canonical_id"]
|
|
86
|
+
|
|
87
|
+
# Prefer inline payload (bulk ops) over canonical object lookup.
|
|
88
|
+
raw_payload = row["data_json"] if row["data_json"] else None
|
|
89
|
+
if raw_payload:
|
|
90
|
+
data = json.loads(raw_payload)
|
|
91
|
+
else:
|
|
92
|
+
obj_data = store.get_object(canonical_id)
|
|
93
|
+
data = obj_data if obj_data else {}
|
|
94
|
+
|
|
95
|
+
# Get resolved dependency IDs (explicit edges)
|
|
96
|
+
deps = conn.execute(
|
|
97
|
+
"""SELECT dep.canonical_id, dep.webex_id
|
|
98
|
+
FROM plan_edges pe
|
|
99
|
+
JOIN plan_operations dep ON dep.node_id = pe.from_node
|
|
100
|
+
WHERE pe.to_node = ? AND dep.status = 'completed'""",
|
|
101
|
+
(node_id,),
|
|
102
|
+
).fetchall()
|
|
103
|
+
resolved_deps = {
|
|
104
|
+
r["canonical_id"]: r["webex_id"]
|
|
105
|
+
for r in deps
|
|
106
|
+
if r["webex_id"]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Also inject all completed location ops into resolved_deps so that
|
|
110
|
+
# handlers can resolve location_id even when no explicit edge exists.
|
|
111
|
+
# This covers CUCM resources (AA, HG, pickup, trunk) that have no
|
|
112
|
+
# location in their canonical form — the handler falls back to
|
|
113
|
+
# _resolve_location() which looks up location_id from data in deps.
|
|
114
|
+
loc_ops = conn.execute(
|
|
115
|
+
"""SELECT canonical_id, webex_id
|
|
116
|
+
FROM plan_operations
|
|
117
|
+
WHERE resource_type = 'location'
|
|
118
|
+
AND op_type = 'create'
|
|
119
|
+
AND status = 'completed'
|
|
120
|
+
AND webex_id IS NOT NULL""",
|
|
121
|
+
).fetchall()
|
|
122
|
+
for loc in loc_ops:
|
|
123
|
+
# Only inject if not already present from explicit edges
|
|
124
|
+
if loc["canonical_id"] not in resolved_deps:
|
|
125
|
+
resolved_deps[loc["canonical_id"]] = loc["webex_id"]
|
|
126
|
+
|
|
127
|
+
result.append({
|
|
128
|
+
"node_id": node_id,
|
|
129
|
+
"resource_type": row["resource_type"],
|
|
130
|
+
"op_type": row["op_type"],
|
|
131
|
+
"canonical_id": canonical_id,
|
|
132
|
+
"description": row["description"] or "",
|
|
133
|
+
"batch": row["batch"],
|
|
134
|
+
"tier": row["tier"],
|
|
135
|
+
"data": data,
|
|
136
|
+
"resolved_deps": resolved_deps,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def update_op_status(
|
|
143
|
+
store: MigrationStore,
|
|
144
|
+
node_id: str,
|
|
145
|
+
status: str,
|
|
146
|
+
webex_id: str | None = None,
|
|
147
|
+
error_message: str | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Update operation status in the DB.
|
|
150
|
+
|
|
151
|
+
Accepted ``status`` values mirror ``models.OpStatus``:
|
|
152
|
+
``pending`` / ``in_progress`` / ``completed`` / ``skipped`` / ``failed``.
|
|
153
|
+
|
|
154
|
+
On completion: sets webex_id, completed_at, clears error_message. Also
|
|
155
|
+
updates the canonical object's webex_id field in the objects table, and
|
|
156
|
+
undoes any prior cascade-skip from this node (so dependents that were
|
|
157
|
+
skipped because THIS op previously failed can now run on retry).
|
|
158
|
+
On failure: sets error_message, increments attempts, cascade-skips
|
|
159
|
+
dependents via hard edges.
|
|
160
|
+
On skip: sets status to 'skipped', stores the reason in error_message,
|
|
161
|
+
and cascade-skips dependents via hard edges (same as failure). Engines
|
|
162
|
+
should pass ``error_message=<reason>`` so the execution report can
|
|
163
|
+
explain why each op was skipped. The ``SkippedResult`` sentinel
|
|
164
|
+
returned by handlers is the canonical producer of this path.
|
|
165
|
+
On in_progress: just flips the status (no error/timestamp updates).
|
|
166
|
+
"""
|
|
167
|
+
conn = store.conn
|
|
168
|
+
|
|
169
|
+
if status == "completed":
|
|
170
|
+
conn.execute(
|
|
171
|
+
"""UPDATE plan_operations
|
|
172
|
+
SET status = ?, webex_id = ?, completed_at = ?,
|
|
173
|
+
error_message = NULL, attempts = attempts + 1
|
|
174
|
+
WHERE node_id = ?""",
|
|
175
|
+
(status, webex_id, _now(), node_id),
|
|
176
|
+
)
|
|
177
|
+
# Also update the canonical object's webex_id
|
|
178
|
+
if webex_id:
|
|
179
|
+
row = conn.execute(
|
|
180
|
+
"SELECT canonical_id FROM plan_operations WHERE node_id = ?",
|
|
181
|
+
(node_id,),
|
|
182
|
+
).fetchone()
|
|
183
|
+
if row:
|
|
184
|
+
canonical_id = row["canonical_id"]
|
|
185
|
+
conn.execute(
|
|
186
|
+
"UPDATE objects SET data = json_set(data, '$.webex_id', ?) WHERE canonical_id = ?",
|
|
187
|
+
(webex_id, canonical_id),
|
|
188
|
+
)
|
|
189
|
+
# Undo cascade-skip: if this op was previously failed and had cascade-
|
|
190
|
+
# skipped its dependents, reset them back to pending so they can execute.
|
|
191
|
+
_undo_cascade_skip(conn, node_id)
|
|
192
|
+
elif status == "failed":
|
|
193
|
+
conn.execute(
|
|
194
|
+
"""UPDATE plan_operations
|
|
195
|
+
SET status = ?, error_message = ?, completed_at = ?,
|
|
196
|
+
attempts = attempts + 1
|
|
197
|
+
WHERE node_id = ?""",
|
|
198
|
+
(status, error_message, _now(), node_id),
|
|
199
|
+
)
|
|
200
|
+
# Cascade: skip all hard dependents since this op failed.
|
|
201
|
+
# The reason_prefix is the full error_message written to each
|
|
202
|
+
# descendant, naming the ROOT failed op so downstream consumers
|
|
203
|
+
# (execution report, cascade_groups grouping) can trace every
|
|
204
|
+
# cascade-skipped op back to a single root cause.
|
|
205
|
+
_cascade_skip(
|
|
206
|
+
conn,
|
|
207
|
+
node_id,
|
|
208
|
+
reason_prefix=f"Cascade skip: dependency {node_id} FAILED",
|
|
209
|
+
)
|
|
210
|
+
elif status == "skipped":
|
|
211
|
+
conn.execute(
|
|
212
|
+
"""UPDATE plan_operations
|
|
213
|
+
SET status = ?, error_message = ?, attempts = attempts + 1
|
|
214
|
+
WHERE node_id = ?""",
|
|
215
|
+
(status, error_message, node_id),
|
|
216
|
+
)
|
|
217
|
+
# Cascade: mark all dependent ops as skipped, referencing the
|
|
218
|
+
# ROOT skipped op so every descendant can be grouped under it.
|
|
219
|
+
_cascade_skip(
|
|
220
|
+
conn,
|
|
221
|
+
node_id,
|
|
222
|
+
reason_prefix=f"Cascade skip: dependency {node_id} SKIPPED",
|
|
223
|
+
)
|
|
224
|
+
elif status == "in_progress":
|
|
225
|
+
conn.execute(
|
|
226
|
+
"UPDATE plan_operations SET status = ? WHERE node_id = ?",
|
|
227
|
+
(status, node_id),
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
raise ValueError(f"Invalid status: {status}")
|
|
231
|
+
|
|
232
|
+
conn.commit()
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cascade_skip(
|
|
236
|
+
conn,
|
|
237
|
+
node_id: str,
|
|
238
|
+
reason_prefix: str = "Cascade skip: dependency <unknown> SKIPPED",
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Recursively skip all ops that depend on the given node via hard edges.
|
|
241
|
+
|
|
242
|
+
SOFT deps are excluded — skipping a user should NOT cascade-skip
|
|
243
|
+
a hunt group that has the user as a SOFT agent dependency.
|
|
244
|
+
|
|
245
|
+
Every descendant at every level receives the SAME ``reason_prefix`` as
|
|
246
|
+
its ``error_message``. The caller (``update_op_status``) builds the
|
|
247
|
+
prefix once to name the ROOT failed/skipped op (e.g.
|
|
248
|
+
``"Cascade skip: dependency create_hg_sales FAILED"``), so a 3-level
|
|
249
|
+
chain A → B → C all produce error_messages referencing A. Downstream
|
|
250
|
+
consumers — the execution report and ``get_execution_progress()``'s
|
|
251
|
+
``cascade_groups`` — parse the prefix to group every cascade-skipped
|
|
252
|
+
op under its single root cause instead of under its immediate parent.
|
|
253
|
+
"""
|
|
254
|
+
dependents = conn.execute(
|
|
255
|
+
"""SELECT pe.to_node FROM plan_edges pe
|
|
256
|
+
JOIN plan_operations po ON po.node_id = pe.to_node
|
|
257
|
+
WHERE pe.from_node = ?
|
|
258
|
+
AND po.status IN ('pending', 'in_progress')
|
|
259
|
+
AND pe.dep_type != 'soft'""",
|
|
260
|
+
(node_id,),
|
|
261
|
+
).fetchall()
|
|
262
|
+
|
|
263
|
+
for dep in dependents:
|
|
264
|
+
dep_node = dep["to_node"]
|
|
265
|
+
conn.execute(
|
|
266
|
+
"""UPDATE plan_operations
|
|
267
|
+
SET status = 'skipped',
|
|
268
|
+
error_message = ?
|
|
269
|
+
WHERE node_id = ? AND status IN ('pending', 'in_progress')""",
|
|
270
|
+
(reason_prefix, dep_node),
|
|
271
|
+
)
|
|
272
|
+
# Recurse — pass the SAME reason_prefix so all descendants
|
|
273
|
+
# reference the root failure, not the intermediate dep_node.
|
|
274
|
+
_cascade_skip(conn, dep_node, reason_prefix=reason_prefix)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _undo_cascade_skip(conn, node_id: str) -> None:
|
|
278
|
+
"""Reset ops that were cascade-skipped due to this node's failure.
|
|
279
|
+
|
|
280
|
+
When a failed op is retried and succeeds, its cascade-skipped dependents
|
|
281
|
+
should be reset to pending so they can execute in the next batch.
|
|
282
|
+
|
|
283
|
+
After the Wave 3B cascade-labeling change, every descendant at every
|
|
284
|
+
depth carries an ``error_message`` that names the ROOT failed op — not
|
|
285
|
+
an intermediate ancestor. So a single exact-match query against the
|
|
286
|
+
root's failure marker is sufficient to reset the entire subtree; no
|
|
287
|
+
recursion is needed.
|
|
288
|
+
|
|
289
|
+
Only the FAILED variant is undone — SKIPPED ops do not transition
|
|
290
|
+
back to success via ``update_op_status`` in normal flow (the
|
|
291
|
+
cucm-migrate skill flow re-creates plan state rather than reversing
|
|
292
|
+
an explicit skip), so undoing a SKIPPED cascade is out of scope.
|
|
293
|
+
"""
|
|
294
|
+
marker = f"Cascade skip: dependency {node_id} FAILED"
|
|
295
|
+
conn.execute(
|
|
296
|
+
"""UPDATE plan_operations
|
|
297
|
+
SET status = 'pending', error_message = NULL
|
|
298
|
+
WHERE status = 'skipped' AND error_message = ?""",
|
|
299
|
+
(marker,),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_completed_ops_for_rollback(
|
|
304
|
+
store: MigrationStore,
|
|
305
|
+
scope: str = "all",
|
|
306
|
+
batch_name: str | None = None,
|
|
307
|
+
) -> list[dict[str, Any]]:
|
|
308
|
+
"""Return completed CREATE ops in reverse dependency order for rollback.
|
|
309
|
+
|
|
310
|
+
Each dict includes:
|
|
311
|
+
- node_id, resource_type, op_type, webex_id, canonical_id
|
|
312
|
+
- data: canonical object data (for name/description in rollback report)
|
|
313
|
+
- location_webex_id: for feature deletes that need LOCATION_ID
|
|
314
|
+
"""
|
|
315
|
+
conn = store.conn
|
|
316
|
+
|
|
317
|
+
if scope == "batch" and batch_name:
|
|
318
|
+
rows = conn.execute(
|
|
319
|
+
"""SELECT node_id, canonical_id, op_type, resource_type,
|
|
320
|
+
webex_id, tier, batch
|
|
321
|
+
FROM plan_operations
|
|
322
|
+
WHERE status = 'completed' AND op_type = 'create'
|
|
323
|
+
AND batch = ?
|
|
324
|
+
ORDER BY tier DESC, node_id DESC""",
|
|
325
|
+
(batch_name,),
|
|
326
|
+
).fetchall()
|
|
327
|
+
else:
|
|
328
|
+
rows = conn.execute(
|
|
329
|
+
"""SELECT node_id, canonical_id, op_type, resource_type,
|
|
330
|
+
webex_id, tier, batch
|
|
331
|
+
FROM plan_operations
|
|
332
|
+
WHERE status = 'completed' AND op_type = 'create'
|
|
333
|
+
ORDER BY tier DESC, node_id DESC"""
|
|
334
|
+
).fetchall()
|
|
335
|
+
|
|
336
|
+
result = []
|
|
337
|
+
for row in rows:
|
|
338
|
+
node_id = row["node_id"]
|
|
339
|
+
canonical_id = row["canonical_id"]
|
|
340
|
+
|
|
341
|
+
obj_data = store.get_object(canonical_id)
|
|
342
|
+
data = obj_data if obj_data else {}
|
|
343
|
+
|
|
344
|
+
# For features, resolve location webex_id from dependencies
|
|
345
|
+
location_webex_id = None
|
|
346
|
+
feature_types = {
|
|
347
|
+
"hunt_group", "call_queue", "auto_attendant",
|
|
348
|
+
"call_park", "pickup_group", "paging_group",
|
|
349
|
+
}
|
|
350
|
+
if row["resource_type"] in feature_types:
|
|
351
|
+
loc_dep = conn.execute(
|
|
352
|
+
"""SELECT dep.webex_id FROM plan_edges pe
|
|
353
|
+
JOIN plan_operations dep ON dep.node_id = pe.from_node
|
|
354
|
+
WHERE pe.to_node = ?
|
|
355
|
+
AND dep.resource_type = 'location'
|
|
356
|
+
AND dep.status = 'completed'""",
|
|
357
|
+
(node_id,),
|
|
358
|
+
).fetchone()
|
|
359
|
+
if loc_dep:
|
|
360
|
+
location_webex_id = loc_dep["webex_id"]
|
|
361
|
+
|
|
362
|
+
result.append({
|
|
363
|
+
"node_id": node_id,
|
|
364
|
+
"resource_type": row["resource_type"],
|
|
365
|
+
"op_type": row["op_type"],
|
|
366
|
+
"webex_id": row["webex_id"],
|
|
367
|
+
"canonical_id": canonical_id,
|
|
368
|
+
"data": data,
|
|
369
|
+
"location_webex_id": location_webex_id,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
return result
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def dry_run_all_batches(store: MigrationStore) -> dict[str, Any]:
|
|
376
|
+
"""Walk all batches in execution order without changing state.
|
|
377
|
+
|
|
378
|
+
For each batch:
|
|
379
|
+
1. Find all ready ops (same logic as get_next_batch)
|
|
380
|
+
2. Simulate completion (mark as 'completed' in a SAVEPOINT)
|
|
381
|
+
3. Move to next batch
|
|
382
|
+
4. ROLLBACK the savepoint — no state changes persist
|
|
383
|
+
|
|
384
|
+
Returns the full execution sequence as a list of batch dicts:
|
|
385
|
+
[{
|
|
386
|
+
"batch": str,
|
|
387
|
+
"tier": int,
|
|
388
|
+
"operations": [
|
|
389
|
+
{"node_id": str, "resource_type": str, "op_type": str,
|
|
390
|
+
"description": str, "resolved_deps": dict}
|
|
391
|
+
]
|
|
392
|
+
}]
|
|
393
|
+
"""
|
|
394
|
+
conn = store.conn
|
|
395
|
+
batches: list[dict[str, Any]] = []
|
|
396
|
+
total_api_calls = 0
|
|
397
|
+
|
|
398
|
+
conn.execute("SAVEPOINT dry_run")
|
|
399
|
+
try:
|
|
400
|
+
while True:
|
|
401
|
+
batch_ops = get_next_batch(store)
|
|
402
|
+
if not batch_ops:
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
batch_name = batch_ops[0]["batch"] or "org-wide"
|
|
406
|
+
tier = batch_ops[0]["tier"]
|
|
407
|
+
|
|
408
|
+
operations = []
|
|
409
|
+
for op in batch_ops:
|
|
410
|
+
api_calls = conn.execute(
|
|
411
|
+
"SELECT api_calls FROM plan_operations WHERE node_id = ?",
|
|
412
|
+
(op["node_id"],),
|
|
413
|
+
).fetchone()
|
|
414
|
+
op_api_calls = api_calls["api_calls"] if api_calls else 1
|
|
415
|
+
total_api_calls += op_api_calls
|
|
416
|
+
|
|
417
|
+
operations.append({
|
|
418
|
+
"node_id": op["node_id"],
|
|
419
|
+
"resource_type": op["resource_type"],
|
|
420
|
+
"op_type": op["op_type"],
|
|
421
|
+
"description": op["description"],
|
|
422
|
+
"resolved_deps": op["resolved_deps"],
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
# Simulate completion so next iteration can resolve deps
|
|
426
|
+
conn.execute(
|
|
427
|
+
"""UPDATE plan_operations
|
|
428
|
+
SET status = 'completed', webex_id = ?
|
|
429
|
+
WHERE node_id = ?""",
|
|
430
|
+
(f"dry-run-{op['node_id']}", op["node_id"]),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
batches.append({
|
|
434
|
+
"batch": batch_name,
|
|
435
|
+
"tier": tier,
|
|
436
|
+
"operations": operations,
|
|
437
|
+
})
|
|
438
|
+
finally:
|
|
439
|
+
conn.execute("ROLLBACK TO dry_run")
|
|
440
|
+
conn.execute("RELEASE dry_run")
|
|
441
|
+
|
|
442
|
+
# Attach summary
|
|
443
|
+
total_ops = sum(len(b["operations"]) for b in batches)
|
|
444
|
+
return {
|
|
445
|
+
"batches": batches,
|
|
446
|
+
"total_operations": total_ops,
|
|
447
|
+
"total_batches": len(batches),
|
|
448
|
+
"total_api_calls": total_api_calls,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
_CASCADE_MSG_RE = re.compile(
|
|
453
|
+
r"^Cascade skip: dependency (?P<root>.+) (?P<status>FAILED|SKIPPED)$"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def get_execution_progress(store: MigrationStore) -> dict[str, Any]:
|
|
458
|
+
"""Return execution progress summary.
|
|
459
|
+
|
|
460
|
+
Returns: {
|
|
461
|
+
"total": int,
|
|
462
|
+
"pending": int,
|
|
463
|
+
"in_progress": int,
|
|
464
|
+
"completed": int,
|
|
465
|
+
"failed": int,
|
|
466
|
+
"skipped": int,
|
|
467
|
+
"by_resource_type": {"location": {"completed": N, "pending": M, ...}, ...},
|
|
468
|
+
"last_error": {"node_id": str, "error": str} | None,
|
|
469
|
+
"last_completed": {"node_id": str, "description": str} | None,
|
|
470
|
+
"cascade_groups": {
|
|
471
|
+
root_node_id: {
|
|
472
|
+
"root_status": "failed" | "skipped",
|
|
473
|
+
"descendants": [node_id, ...],
|
|
474
|
+
},
|
|
475
|
+
...
|
|
476
|
+
},
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
The ``cascade_groups`` section parses the cascade-skip error_message
|
|
480
|
+
format emitted by ``_cascade_skip`` (``"Cascade skip: dependency
|
|
481
|
+
{root} FAILED"`` or ``... SKIPPED``) and groups every cascade-skipped
|
|
482
|
+
op under the single root cause it traces back to. Directly-skipped
|
|
483
|
+
ops (SkippedResult from a handler, not cascaded) are NOT included —
|
|
484
|
+
they carry a handler-supplied reason, not the cascade marker.
|
|
485
|
+
"""
|
|
486
|
+
conn = store.conn
|
|
487
|
+
|
|
488
|
+
# Overall counts
|
|
489
|
+
status_rows = conn.execute(
|
|
490
|
+
"""SELECT status, COUNT(*) as cnt
|
|
491
|
+
FROM plan_operations GROUP BY status"""
|
|
492
|
+
).fetchall()
|
|
493
|
+
counts = {r["status"]: r["cnt"] for r in status_rows}
|
|
494
|
+
|
|
495
|
+
total = sum(counts.values())
|
|
496
|
+
progress = {
|
|
497
|
+
"total": total,
|
|
498
|
+
"pending": counts.get("pending", 0),
|
|
499
|
+
"in_progress": counts.get("in_progress", 0),
|
|
500
|
+
"completed": counts.get("completed", 0),
|
|
501
|
+
"failed": counts.get("failed", 0),
|
|
502
|
+
"skipped": counts.get("skipped", 0),
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Per-resource-type breakdown
|
|
506
|
+
rt_rows = conn.execute(
|
|
507
|
+
"""SELECT resource_type, status, COUNT(*) as cnt
|
|
508
|
+
FROM plan_operations GROUP BY resource_type, status"""
|
|
509
|
+
).fetchall()
|
|
510
|
+
by_rt: dict[str, dict[str, int]] = {}
|
|
511
|
+
for r in rt_rows:
|
|
512
|
+
rt = r["resource_type"]
|
|
513
|
+
if rt not in by_rt:
|
|
514
|
+
by_rt[rt] = {}
|
|
515
|
+
by_rt[rt][r["status"]] = r["cnt"]
|
|
516
|
+
progress["by_resource_type"] = by_rt
|
|
517
|
+
|
|
518
|
+
# Last error
|
|
519
|
+
err_row = conn.execute(
|
|
520
|
+
"""SELECT node_id, error_message
|
|
521
|
+
FROM plan_operations
|
|
522
|
+
WHERE status = 'failed' AND error_message IS NOT NULL
|
|
523
|
+
ORDER BY completed_at DESC, node_id DESC
|
|
524
|
+
LIMIT 1"""
|
|
525
|
+
).fetchone()
|
|
526
|
+
progress["last_error"] = (
|
|
527
|
+
{"node_id": err_row["node_id"], "error": err_row["error_message"]}
|
|
528
|
+
if err_row
|
|
529
|
+
else None
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Last completed
|
|
533
|
+
comp_row = conn.execute(
|
|
534
|
+
"""SELECT node_id, description
|
|
535
|
+
FROM plan_operations
|
|
536
|
+
WHERE status = 'completed'
|
|
537
|
+
ORDER BY completed_at DESC
|
|
538
|
+
LIMIT 1"""
|
|
539
|
+
).fetchone()
|
|
540
|
+
progress["last_completed"] = (
|
|
541
|
+
{"node_id": comp_row["node_id"], "description": comp_row["description"]}
|
|
542
|
+
if comp_row
|
|
543
|
+
else None
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Cascade groups: parse the cascade-skip error_message format to group
|
|
547
|
+
# every cascade-skipped op under its single root cause.
|
|
548
|
+
cascade_rows = conn.execute(
|
|
549
|
+
"""SELECT node_id, error_message
|
|
550
|
+
FROM plan_operations
|
|
551
|
+
WHERE status = 'skipped'
|
|
552
|
+
AND error_message LIKE 'Cascade skip: dependency %'
|
|
553
|
+
ORDER BY node_id"""
|
|
554
|
+
).fetchall()
|
|
555
|
+
|
|
556
|
+
cascade_groups: dict[str, dict[str, Any]] = {}
|
|
557
|
+
for row in cascade_rows:
|
|
558
|
+
match = _CASCADE_MSG_RE.match(row["error_message"] or "")
|
|
559
|
+
if not match:
|
|
560
|
+
continue
|
|
561
|
+
root = match.group("root")
|
|
562
|
+
root_status = match.group("status").lower() # "failed" | "skipped"
|
|
563
|
+
group = cascade_groups.setdefault(
|
|
564
|
+
root,
|
|
565
|
+
{"root_status": root_status, "descendants": []},
|
|
566
|
+
)
|
|
567
|
+
group["descendants"].append(row["node_id"])
|
|
568
|
+
|
|
569
|
+
progress["cascade_groups"] = cascade_groups
|
|
570
|
+
|
|
571
|
+
return progress
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# export/ — Phase 09 → 12b: Export Layer
|
|
2
|
+
|
|
3
|
+
Generates summary deployment plans and data exports from the SQLite migration store.
|
|
4
|
+
|
|
5
|
+
Phase 12b removed the command_builder (static CLI string generation) in favor of
|
|
6
|
+
skill-delegated execution. The deployment plan is now summary-only — no CLI commands,
|
|
7
|
+
no placeholders. The cucm-migrate skill queries the DB for operation metadata and
|
|
8
|
+
delegates to domain skills at runtime.
|
|
9
|
+
|
|
10
|
+
## Files
|
|
11
|
+
|
|
12
|
+
| File | Purpose |
|
|
13
|
+
|------|---------|
|
|
14
|
+
| `deployment_plan.py` | Generates 8-section summary plan for admin review (no CLI commands) |
|
|
15
|
+
| `json_export.py` | Full JSON export (objects, decisions, cross-refs, plan ops/edges) |
|
|
16
|
+
| `csv_export.py` | CSV decisions export for stakeholder review |
|
|
17
|
+
|
|
18
|
+
## Key Design Decisions
|
|
19
|
+
|
|
20
|
+
### Summary-only deployment plan (Phase 12b)
|
|
21
|
+
|
|
22
|
+
The deployment plan no longer contains CLI commands or `{STEP_N_ID}` placeholders.
|
|
23
|
+
It shows: objective, prerequisites, resource summary (Webex types only), decisions
|
|
24
|
+
made, batch execution order, estimated impact, rollback strategy, and approval.
|
|
25
|
+
The cucm-migrate skill uses `wxcli cucm next-batch` to get operation metadata and
|
|
26
|
+
delegates execution to domain skills.
|
|
27
|
+
|
|
28
|
+
### --json-body bypasses required flags (Phase 12b)
|
|
29
|
+
|
|
30
|
+
The generator now makes all required body fields `typer.Option(None)` when
|
|
31
|
+
`--json-body` is present. Runtime validation checks for missing required fields
|
|
32
|
+
only when `--json-body` is NOT used. This allows domain skills to pass
|
|
33
|
+
`--json-body '{"name":"X",...}'` without also specifying `--name X`.
|
|
34
|
+
|
|
35
|
+
### License assignment at create time
|
|
36
|
+
|
|
37
|
+
`wxcli users update` does NOT have a `--license` flag. License assignment uses
|
|
38
|
+
`--json-body` on update. License and extension are included in the `user:create` step.
|
|
39
|
+
Note: `wxcli users` is now an alias for the generated `people` command group.
|
|
40
|
+
|
|
41
|
+
### Feature delete commands need LOCATION_ID
|
|
42
|
+
|
|
43
|
+
All feature delete commands require `LOCATION_ID RESOURCE_ID` as two positional
|
|
44
|
+
arguments. Rollback uses `get_completed_ops_for_rollback()` which resolves the
|
|
45
|
+
location_webex_id from dependencies.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
- `tests/migration/export/test_deployment_plan.py` — summary plan tests
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Export layer — converts migration plan into deployment artifacts.
|
|
2
|
+
|
|
3
|
+
Produces deployment plans (markdown), JSON exports, and CSV decision reports
|
|
4
|
+
from the SQLite migration store. The deployment plan format matches
|
|
5
|
+
docs/templates/deployment-plan.md and is consumable by the wxc-calling-builder agent.
|
|
6
|
+
|
|
7
|
+
(Phase 09 — bridge to wxc-calling-builder)
|
|
8
|
+
"""
|