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,303 @@
|
|
|
1
|
+
"""Execution planning layer — models, constants, and tier assignments.
|
|
2
|
+
|
|
3
|
+
Defines the data structures for the execution DAG: operations (nodes),
|
|
4
|
+
dependency types (edge labels), broken cycles, and batches. Also contains
|
|
5
|
+
the static tier assignments and API call estimates sourced from
|
|
6
|
+
05-dependency-graph.md.
|
|
7
|
+
|
|
8
|
+
(from 05-dependency-graph.md — MigrationOp, DependencyType, batch model)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Enums
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
class DependencyType(str, Enum):
|
|
23
|
+
"""Edge types in the dependency DAG.
|
|
24
|
+
(from 05-dependency-graph.md lines 104-108)
|
|
25
|
+
"""
|
|
26
|
+
REQUIRES = "requires" # hard: A must exist before B can be created
|
|
27
|
+
CONFIGURES = "configures" # hard: A must be created before settings applied
|
|
28
|
+
SOFT = "soft" # breakable: nice-to-have ordering
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Models
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
class MigrationOp(BaseModel):
|
|
36
|
+
"""One node in the dependency DAG.
|
|
37
|
+
(from 05-dependency-graph.md lines 22-32)
|
|
38
|
+
"""
|
|
39
|
+
canonical_id: str # links back to objects table
|
|
40
|
+
op_type: str # "create", "configure", "assign", etc.
|
|
41
|
+
resource_type: str # "location", "user", "device", etc.
|
|
42
|
+
tier: int # from TIER_ASSIGNMENTS
|
|
43
|
+
batch: str | None = None # site/batch assignment
|
|
44
|
+
api_calls: int = 1 # estimated API call count for rate limit budgeting
|
|
45
|
+
description: str = "" # human-readable: "Create user jsmith@acme.com"
|
|
46
|
+
depends_on: list[str] = Field(default_factory=list) # node_ids this op must wait for
|
|
47
|
+
payload: dict | None = None # Bulk ops: handler data that doesn't come from a canonical object
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BrokenCycle(BaseModel):
|
|
51
|
+
"""Record of a cycle that was broken during DAG construction.
|
|
52
|
+
(from 05-dependency-graph.md lines 186-193)
|
|
53
|
+
"""
|
|
54
|
+
from_node: str
|
|
55
|
+
to_node: str
|
|
56
|
+
dep_type: str | None = None
|
|
57
|
+
reason: str = ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Batch(BaseModel):
|
|
61
|
+
"""A group of operations to execute together.
|
|
62
|
+
(from 05-dependency-graph.md lines 244-258)
|
|
63
|
+
"""
|
|
64
|
+
site: str # "org-wide" or a location canonical_id
|
|
65
|
+
tier: int
|
|
66
|
+
operations: list[str] = Field(default_factory=list) # node_ids
|
|
67
|
+
estimated_api_calls: int = 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Static tier assignments
|
|
72
|
+
# (from 05-dependency-graph.md — tier system lines 79-100, batch ordering
|
|
73
|
+
# lines 206-216, expand_to_operations example lines 41-74)
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
TIER_ASSIGNMENTS: dict[tuple[str, str], int] = {
|
|
77
|
+
# Tier 0: Infrastructure — locations created first, then calling enabled
|
|
78
|
+
("location", "create"): 0,
|
|
79
|
+
("location", "enable_calling"): 0,
|
|
80
|
+
# Tier 1: Routing backbone — trunks, schedules, route groups, line key templates
|
|
81
|
+
("trunk", "create"): 1,
|
|
82
|
+
("route_group", "create"): 1,
|
|
83
|
+
("route_list", "create"): 1,
|
|
84
|
+
("route_list", "configure_numbers"): 1,
|
|
85
|
+
("operating_mode", "create"): 1,
|
|
86
|
+
("schedule", "create"): 1,
|
|
87
|
+
("line_key_template", "create"): 1,
|
|
88
|
+
# Tier 2: People creation + org-wide routing objects
|
|
89
|
+
("user", "create"): 2,
|
|
90
|
+
("workspace", "create"): 2,
|
|
91
|
+
("dial_plan", "create"): 2,
|
|
92
|
+
("translation_pattern", "create"): 2,
|
|
93
|
+
# calling_permission has no "create" op — permissions are per-user, not standalone
|
|
94
|
+
# Tier 3: Numbers, devices (Fix 12: license+number folded into create)
|
|
95
|
+
("workspace", "assign_number"): 3,
|
|
96
|
+
("device", "create"): 3,
|
|
97
|
+
("device", "create_activation_code"): 3,
|
|
98
|
+
# Tier 4: Call features (depend on users/workspaces existing)
|
|
99
|
+
("hunt_group", "create"): 4,
|
|
100
|
+
("call_queue", "create"): 4,
|
|
101
|
+
("auto_attendant", "create"): 4,
|
|
102
|
+
("call_park", "create"): 4,
|
|
103
|
+
("pickup_group", "create"): 4,
|
|
104
|
+
("paging_group", "create"): 4,
|
|
105
|
+
("voicemail_group", "create"): 4,
|
|
106
|
+
# Tier 5: Settings configuration (depends on licensed users)
|
|
107
|
+
("user", "configure_settings"): 5,
|
|
108
|
+
("user", "configure_voicemail"): 5,
|
|
109
|
+
("workspace", "configure_settings"): 5,
|
|
110
|
+
("device", "configure_settings"): 5,
|
|
111
|
+
("calling_permission", "assign"): 5,
|
|
112
|
+
("call_forwarding", "configure"): 5,
|
|
113
|
+
("single_number_reach", "configure"): 5,
|
|
114
|
+
# Feature forwarding/holiday/night service configuration
|
|
115
|
+
# (from 2026-04-10-feature-forwarding-night-service.md)
|
|
116
|
+
("hunt_group", "configure_forwarding"): 5,
|
|
117
|
+
("call_queue", "configure_forwarding"): 5,
|
|
118
|
+
("call_queue", "configure_holiday_service"): 5,
|
|
119
|
+
("call_queue", "configure_night_service"): 5,
|
|
120
|
+
("call_queue", "configure_stranded_calls"): 5,
|
|
121
|
+
("auto_attendant", "configure_forwarding"): 5,
|
|
122
|
+
("ecbn_config", "configure"): 5,
|
|
123
|
+
# Tier 6: Shared/virtual lines + monitoring (depend on users + devices)
|
|
124
|
+
("shared_line", "configure"): 6,
|
|
125
|
+
("virtual_line", "create"): 6,
|
|
126
|
+
("virtual_line", "configure"): 6,
|
|
127
|
+
("monitoring_list", "configure"): 6,
|
|
128
|
+
("receptionist_config", "configure"): 6,
|
|
129
|
+
# Tier 7: Device finalization + cycle fixups
|
|
130
|
+
# NOTE: tier 7 is shared between device finalization and cycle-break fixups.
|
|
131
|
+
# Fixups use batch="fixups"; device ops use location-derived batches — no conflict.
|
|
132
|
+
("device_layout", "configure"): 7,
|
|
133
|
+
("softkey_config", "configure"): 7,
|
|
134
|
+
# Device settings templates — location-level config at tier 1, per-device overrides at tier 5
|
|
135
|
+
("device_settings_template", "apply_location_settings"): 1,
|
|
136
|
+
("device_settings_template", "apply_device_override"): 5,
|
|
137
|
+
# Hoteling / Hot Desking (depends on users + devices)
|
|
138
|
+
("device_profile", "enable_hoteling_guest"): 5,
|
|
139
|
+
("device_profile", "enable_hoteling_host"): 5,
|
|
140
|
+
("hoteling_location", "enable_hotdesking"): 0, # Same tier as location:enable_calling
|
|
141
|
+
# Bulk job operations (Phase: bulk-operations)
|
|
142
|
+
("bulk_device_settings", "submit"): 5,
|
|
143
|
+
("bulk_line_key_template", "submit"): 7,
|
|
144
|
+
("bulk_dynamic_settings", "submit"): 7,
|
|
145
|
+
("bulk_rebuild_phones", "submit"): 8, # Tier 8: runs after all device finalization
|
|
146
|
+
# DECT network provisioning (depends on location, users/workspaces)
|
|
147
|
+
("dect_network", "create"): 2, # Create DECT network at location
|
|
148
|
+
("dect_network", "create_base_stations"): 2, # Register base stations with MACs
|
|
149
|
+
("dect_network", "assign_handsets"): 3, # Assign handsets (after users/workspaces created)
|
|
150
|
+
# Advisory-to-execution bridge (Phase A: no-op placeholders)
|
|
151
|
+
("music_on_hold", "configure"): 5, # Settings tier — location must exist + calling enabled
|
|
152
|
+
("announcement", "upload"): 1, # Routing backbone — features reference announcements
|
|
153
|
+
# Executive/assistant pairing (from 2026-04-10-executive-assistant-migration.md §4e)
|
|
154
|
+
# Type assignments must precede pairing; pairing must precede alert/filtering/screening.
|
|
155
|
+
# Steps 4-7 (alert, filtering, screening, assistant settings) can run in parallel after step 3.
|
|
156
|
+
("executive_assistant", "assign_executive_type"): 5,
|
|
157
|
+
("executive_assistant", "assign_assistant_type"): 5,
|
|
158
|
+
("executive_assistant", "assign_assistants"): 5,
|
|
159
|
+
("executive_assistant", "configure_alert"): 5,
|
|
160
|
+
("executive_assistant", "configure_filtering"): 5,
|
|
161
|
+
("executive_assistant", "configure_screening"): 5,
|
|
162
|
+
("executive_assistant", "configure_assistant_settings"): 5,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# API call estimates per operation
|
|
167
|
+
# (from 05-dependency-graph.md lines 268-281)
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
API_CALL_ESTIMATES: dict[str, int] = {
|
|
171
|
+
# Tier 0: Infrastructure
|
|
172
|
+
"location:create": 1, # POST /locations (from provisioning.md)
|
|
173
|
+
"location:enable_calling": 1, # POST /telephony/config/locations (Fix 13)
|
|
174
|
+
# Tier 1: Routing backbone + org-wide infrastructure
|
|
175
|
+
"trunk:create": 1, # POST /telephony/config/premisePstn/trunks (from call-routing.md)
|
|
176
|
+
"route_group:create": 1, # POST /telephony/config/premisePstn/routeGroups (from call-routing.md)
|
|
177
|
+
"route_list:create": 1, # POST /telephony/config/premisePstn/routeLists
|
|
178
|
+
"route_list:configure_numbers": 1, # PUT /telephony/config/premisePstn/routeLists/{id}/numbers
|
|
179
|
+
"operating_mode:create": 1, # POST /telephony/config/operatingModes (from location-call-settings-media.md)
|
|
180
|
+
"schedule:create": 1, # POST /telephony/config/locations/{id}/schedules (from location-call-settings-media.md)
|
|
181
|
+
"line_key_template:create": 1, # POST /telephony/config/devices/lineKeyTemplates
|
|
182
|
+
# Tier 2: People + org-wide routing
|
|
183
|
+
"user:create": 1, # POST /people (from provisioning.md) — Fix 12: includes license + extension
|
|
184
|
+
"user:configure_settings": 5, # ~5 PUT calls: callForwarding, callerId, callWaiting, doNotDisturb,
|
|
185
|
+
# + 1 more (monitoring/recording/intercept) (from person-call-settings-*.md)
|
|
186
|
+
"user:configure_voicemail": 1, # PUT /people/{id}/features/voicemail (from person-call-settings-media.md)
|
|
187
|
+
"workspace:create": 1, # POST /workspaces with calling config (from devices-workspaces.md)
|
|
188
|
+
"workspace:assign_number": 1, # PUT number assignment if DID needed beyond extension in create
|
|
189
|
+
# (from devices-workspaces.md Raw HTTP section)
|
|
190
|
+
"workspace:configure_settings": 3, # ~3 PUT calls: callForwarding, callWaiting, callerId
|
|
191
|
+
# (from devices-workspaces.md Workspace Call Settings section)
|
|
192
|
+
"device:create": 1, # POST /devices by MAC or POST /devices/activationCode
|
|
193
|
+
# (from devices-core.md lines 1312-1323, 1304-1308)
|
|
194
|
+
"device:create_activation_code": 1, # POST /devices/activationCode (from devices-core.md)
|
|
195
|
+
"device:configure_settings": 1, # PUT /telephony/config/devices/{id}/settings (from devices-core.md)
|
|
196
|
+
"dial_plan:create": 1, # POST /telephony/config/premisePstn/dialPlans (from call-routing.md)
|
|
197
|
+
"translation_pattern:create": 1, # POST /telephony/config/callRouting/translationPatterns (from call-routing.md)
|
|
198
|
+
"calling_permission:create": 0, # logical grouping only — no standalone API (from person-call-settings-permissions.md)
|
|
199
|
+
"calling_permission:assign": 1, # PUT /people/{id}/features/outgoingPermission per user
|
|
200
|
+
# (from person-call-settings-permissions.md line 401)
|
|
201
|
+
"call_forwarding:configure": 1, # PUT /people/{id}/features/callForwarding
|
|
202
|
+
"single_number_reach:configure": 2, # PUT SNR enable + POST per number
|
|
203
|
+
# Feature forwarding/holiday/night service configuration
|
|
204
|
+
"hunt_group:configure_forwarding": 1, # PUT .../huntGroups/{id}/callForwarding
|
|
205
|
+
"call_queue:configure_forwarding": 1, # PUT .../queues/{id}/callForwarding
|
|
206
|
+
"call_queue:configure_holiday_service": 1, # PUT .../queues/{id}/holidayService
|
|
207
|
+
"call_queue:configure_night_service": 1, # PUT .../queues/{id}/nightService
|
|
208
|
+
"call_queue:configure_stranded_calls": 1, # PUT .../queues/{id}/strandedCalls
|
|
209
|
+
"auto_attendant:configure_forwarding": 1, # PUT .../autoAttendants/{id}/callForwarding
|
|
210
|
+
"ecbn_config:configure": 1, # PUT /telephony/config/{kind}/{id}/emergencyCallbackNumber
|
|
211
|
+
# Tier 4: Call features
|
|
212
|
+
"hunt_group:create": 1, # POST /telephony/config/locations/{id}/huntGroups
|
|
213
|
+
# agents optional at create (from call-features-major.md line 894)
|
|
214
|
+
"call_queue:create": 1, # POST /telephony/config/locations/{id}/queues
|
|
215
|
+
# (from call-features-major.md lines 616-627)
|
|
216
|
+
"auto_attendant:create": 1, # POST /telephony/config/locations/{id}/autoAttendants — menu is part of
|
|
217
|
+
# the create body, not a separate PUT (from call-features-major.md line 184)
|
|
218
|
+
"call_park:create": 1, # POST /telephony/config/locations/{id}/callParks (from call-features-additional.md)
|
|
219
|
+
"pickup_group:create": 1, # POST /telephony/config/locations/{id}/callPickups (from call-features-additional.md)
|
|
220
|
+
"paging_group:create": 1, # POST /telephony/config/locations/{id}/paging (from call-features-additional.md)
|
|
221
|
+
"voicemail_group:create": 1, # POST /telephony/config/locations/{id}/voicemailGroups
|
|
222
|
+
# Tier 6: Shared/virtual lines + monitoring
|
|
223
|
+
"shared_line:configure": 2, # Approximate — one PUT per owner to /people/{id}/applications/members
|
|
224
|
+
# (from person-call-settings-behavior.md §4)
|
|
225
|
+
"virtual_line:create": 1, # POST /telephony/config/virtualLines (from virtual-lines.md lines 93-104)
|
|
226
|
+
"virtual_line:configure": 1, # PUT /telephony/config/virtualLines/{id} (from virtual-lines.md lines 132-147)
|
|
227
|
+
"monitoring_list:configure": 1,
|
|
228
|
+
"receptionist_config:configure": 2, # PUT reception + POST directory # PUT /people/{id}/features/monitoring
|
|
229
|
+
# Tier 7: Device finalization
|
|
230
|
+
"device_layout:configure": 3, # PUT members + PUT layout + POST applyChanges
|
|
231
|
+
"softkey_config:configure": 2, # PUT dynamicSettings + POST applyChanges
|
|
232
|
+
# Device settings templates
|
|
233
|
+
"device_settings_template:apply_location_settings": 1, # PUT /telephony/config/locations/{id}/devices/settings
|
|
234
|
+
"device_settings_template:apply_device_override": 1, # PUT /telephony/config/devices/{id}/settings
|
|
235
|
+
# Hoteling / Hot Desking
|
|
236
|
+
"device_profile:enable_hoteling_guest": 1, # PUT /people/{id}/features/hoteling
|
|
237
|
+
"device_profile:enable_hoteling_host": 1, # PUT /telephony/config/people/{id}/devices/settings/hoteling
|
|
238
|
+
"hoteling_location:enable_hotdesking": 1, # PUT /telephony/config/locations/{id}/features/hotDesking
|
|
239
|
+
# Bulk job operations (submit=1 + ~5-10 polling calls; conservatively estimated at 10)
|
|
240
|
+
"bulk_device_settings:submit": 10,
|
|
241
|
+
"bulk_line_key_template:submit": 10,
|
|
242
|
+
"bulk_dynamic_settings:submit": 10,
|
|
243
|
+
"bulk_rebuild_phones:submit": 10,
|
|
244
|
+
# DECT network provisioning
|
|
245
|
+
"dect_network:create": 1, # POST /telephony/config/locations/{id}/dectNetworks
|
|
246
|
+
"dect_network:create_base_stations": 1, # POST /telephony/config/locations/{id}/dectNetworks/{id}/baseStations
|
|
247
|
+
"dect_network:assign_handsets": 1, # POST /telephony/config/locations/{id}/dectNetworks/{id}/handsets
|
|
248
|
+
# Advisory-to-execution bridge (Phase A: no-op placeholders, 0 API calls)
|
|
249
|
+
"music_on_hold:configure": 0,
|
|
250
|
+
"announcement:upload": 0,
|
|
251
|
+
# Executive/assistant pairing
|
|
252
|
+
"executive_assistant:assign_executive_type": 1, # PUT /people/{id}/features/executiveAssistant
|
|
253
|
+
"executive_assistant:assign_assistant_type": 2, # PUT per assistant (typically 1-3)
|
|
254
|
+
"executive_assistant:assign_assistants": 1, # PUT /telephony/config/people/{id}/executive/assignedAssistants
|
|
255
|
+
"executive_assistant:configure_alert": 1, # PUT /telephony/config/people/{id}/executive/alert
|
|
256
|
+
"executive_assistant:configure_filtering": 1, # PUT /telephony/config/people/{id}/executive/callFiltering
|
|
257
|
+
"executive_assistant:configure_screening": 1, # PUT /telephony/config/people/{id}/executive/screening
|
|
258
|
+
"executive_assistant:configure_assistant_settings": 2, # PUT per assistant /executive/assistant
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Org-wide resource types — these go into the "org-wide" batch, not per-site
|
|
263
|
+
# (from 05-dependency-graph.md batch ordering lines 206-216)
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
ORG_WIDE_TYPES: set[str] = {
|
|
267
|
+
"location",
|
|
268
|
+
"trunk",
|
|
269
|
+
"route_group",
|
|
270
|
+
"route_list",
|
|
271
|
+
"operating_mode",
|
|
272
|
+
"dial_plan",
|
|
273
|
+
"translation_pattern",
|
|
274
|
+
"calling_permission",
|
|
275
|
+
"line_key_template", # Org-scoped; not tied to a specific location
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Bulk job support
|
|
280
|
+
# (from docs/superpowers/specs/2026-04-10-bulk-operations.md §3a, §4c)
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
BULK_DEVICE_THRESHOLD_DEFAULT: int = 100
|
|
284
|
+
|
|
285
|
+
# Resource types whose ops must run sequentially within a tier, not via
|
|
286
|
+
# asyncio.gather(). Driven by Webex's one-job-per-org constraint for these
|
|
287
|
+
# job families (spec §4c, §5a).
|
|
288
|
+
SERIALIZED_RESOURCE_TYPES: set[str] = {
|
|
289
|
+
"bulk_device_settings",
|
|
290
|
+
"bulk_line_key_template",
|
|
291
|
+
"bulk_dynamic_settings",
|
|
292
|
+
"bulk_rebuild_phones",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Maps internal bulk resource_type to the Webex jobType path segment used
|
|
296
|
+
# for polling and error-fetch URLs:
|
|
297
|
+
# GET /v1/telephony/config/jobs/devices/{jobType}/{jobId}[/errors]
|
|
298
|
+
BULK_JOB_TYPES: dict[str, str] = {
|
|
299
|
+
"bulk_device_settings": "callDeviceSettings",
|
|
300
|
+
"bulk_line_key_template": "applyLineKeyTemplate",
|
|
301
|
+
"bulk_dynamic_settings": "dynamicDeviceSettings",
|
|
302
|
+
"bulk_rebuild_phones": "rebuildPhones",
|
|
303
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Batch partitioning — split the DAG into executable batches.
|
|
2
|
+
|
|
3
|
+
Two-level partitioning:
|
|
4
|
+
1. Org-wide vs site-specific (based on batch assignment on each node)
|
|
5
|
+
2. Within each group, split by tier
|
|
6
|
+
|
|
7
|
+
Batch ordering:
|
|
8
|
+
1. org-wide tiers 0-1 (locations, trunks, schedules, route groups)
|
|
9
|
+
2. org-wide tier 2 (dial plans, calling permissions)
|
|
10
|
+
3. Per-site tiers 2-6 (users, devices, features, settings)
|
|
11
|
+
4. Fixups tier 7 (broken cycle fixups)
|
|
12
|
+
|
|
13
|
+
(from 05-dependency-graph.md lines 202-261)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import math
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
|
|
23
|
+
import networkx as nx
|
|
24
|
+
|
|
25
|
+
from wxcli.migration.execute import Batch, DependencyType
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def partition_into_batches(
|
|
31
|
+
G: nx.DiGraph,
|
|
32
|
+
rate_limit_per_minute: int = 100,
|
|
33
|
+
) -> list[Batch]:
|
|
34
|
+
"""Partition the DAG into executable batches.
|
|
35
|
+
|
|
36
|
+
Batches are ordered: org-wide first, then per-site, then fixups.
|
|
37
|
+
Within each group, split by tier. Returns a list of Batch objects
|
|
38
|
+
with estimated API call counts and timing.
|
|
39
|
+
|
|
40
|
+
(from 05-dependency-graph.md lines 221-258)
|
|
41
|
+
"""
|
|
42
|
+
# Separate nodes by batch assignment
|
|
43
|
+
org_wide_nodes: list[str] = []
|
|
44
|
+
fixup_nodes: list[str] = []
|
|
45
|
+
site_nodes: dict[str, list[str]] = defaultdict(list)
|
|
46
|
+
|
|
47
|
+
for node in nx.topological_sort(G):
|
|
48
|
+
batch = G.nodes[node].get("batch")
|
|
49
|
+
|
|
50
|
+
if batch == "fixups":
|
|
51
|
+
fixup_nodes.append(node)
|
|
52
|
+
elif batch is None or batch == "org-wide":
|
|
53
|
+
org_wide_nodes.append(node)
|
|
54
|
+
else:
|
|
55
|
+
site_nodes[batch].append(node)
|
|
56
|
+
|
|
57
|
+
batches: list[Batch] = []
|
|
58
|
+
|
|
59
|
+
# 1. Org-wide batches first, split by tier
|
|
60
|
+
batches.extend(_split_by_tier(org_wide_nodes, G, "org-wide"))
|
|
61
|
+
|
|
62
|
+
# 2. Per-site batches, sorted by site name for determinism
|
|
63
|
+
for site in sorted(site_nodes.keys()):
|
|
64
|
+
batches.extend(_split_by_tier(site_nodes[site], G, site))
|
|
65
|
+
|
|
66
|
+
# 3. Fixup batch last
|
|
67
|
+
if fixup_nodes:
|
|
68
|
+
batches.extend(_split_by_tier(fixup_nodes, G, "fixups"))
|
|
69
|
+
|
|
70
|
+
# Log summary
|
|
71
|
+
total_ops = sum(len(b.operations) for b in batches)
|
|
72
|
+
total_api = sum(b.estimated_api_calls for b in batches)
|
|
73
|
+
logger.info(
|
|
74
|
+
"Partitioned %d operations into %d batches "
|
|
75
|
+
"(estimated %d API calls, ~%d min at %d req/min)",
|
|
76
|
+
total_ops,
|
|
77
|
+
len(batches),
|
|
78
|
+
total_api,
|
|
79
|
+
math.ceil(total_api / rate_limit_per_minute) if total_api else 0,
|
|
80
|
+
rate_limit_per_minute,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return batches
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _split_by_tier(
|
|
87
|
+
nodes: list[str],
|
|
88
|
+
G: nx.DiGraph,
|
|
89
|
+
site: str,
|
|
90
|
+
) -> list[Batch]:
|
|
91
|
+
"""Split a set of nodes into one batch per tier.
|
|
92
|
+
|
|
93
|
+
(from 05-dependency-graph.md lines 244-258)
|
|
94
|
+
"""
|
|
95
|
+
by_tier: dict[int, list[str]] = defaultdict(list)
|
|
96
|
+
for n in nodes:
|
|
97
|
+
tier = G.nodes[n].get("tier", 0)
|
|
98
|
+
by_tier[tier].append(n)
|
|
99
|
+
|
|
100
|
+
batches = []
|
|
101
|
+
for tier in sorted(by_tier.keys()):
|
|
102
|
+
tier_nodes = by_tier[tier]
|
|
103
|
+
api_calls = sum(G.nodes[n].get("api_calls", 1) for n in tier_nodes)
|
|
104
|
+
batches.append(Batch(
|
|
105
|
+
site=site,
|
|
106
|
+
tier=tier,
|
|
107
|
+
operations=tier_nodes,
|
|
108
|
+
estimated_api_calls=api_calls,
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
return batches
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def format_batch_plan(
|
|
115
|
+
batches: list[Batch],
|
|
116
|
+
rate_limit_per_minute: int = 100,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Format a human-readable execution plan from batches.
|
|
119
|
+
|
|
120
|
+
(from 05-dependency-graph.md lines 285-295 — plan output format)
|
|
121
|
+
"""
|
|
122
|
+
if not batches:
|
|
123
|
+
return "No batches to execute."
|
|
124
|
+
|
|
125
|
+
lines: list[str] = ["Execution Plan", "=" * 60]
|
|
126
|
+
|
|
127
|
+
total_api = 0
|
|
128
|
+
for batch in batches:
|
|
129
|
+
total_api += batch.estimated_api_calls
|
|
130
|
+
est_minutes = math.ceil(batch.estimated_api_calls / rate_limit_per_minute) if batch.estimated_api_calls else 0
|
|
131
|
+
|
|
132
|
+
# Count operations by type
|
|
133
|
+
op_counts: dict[str, int] = defaultdict(int)
|
|
134
|
+
for node_id in batch.operations:
|
|
135
|
+
# node_id format: "canonical_id:op_type"
|
|
136
|
+
parts = node_id.rsplit(":", 1)
|
|
137
|
+
if len(parts) == 2:
|
|
138
|
+
op_counts[parts[1]] += 1
|
|
139
|
+
else:
|
|
140
|
+
op_counts["unknown"] += 1
|
|
141
|
+
|
|
142
|
+
op_summary = ", ".join(
|
|
143
|
+
f"{count} {op_type}" for op_type, count in sorted(op_counts.items())
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
lines.append(f"\nBatch: {batch.site} / tier {batch.tier}")
|
|
147
|
+
lines.append(f" Operations: {op_summary}")
|
|
148
|
+
lines.append(f" Estimated API calls: {batch.estimated_api_calls}")
|
|
149
|
+
if est_minutes > 0:
|
|
150
|
+
lines.append(f" Estimated time at {rate_limit_per_minute} req/min: ~{est_minutes} minutes")
|
|
151
|
+
|
|
152
|
+
total_minutes = math.ceil(total_api / rate_limit_per_minute) if total_api else 0
|
|
153
|
+
lines.append(f"\n{'=' * 60}")
|
|
154
|
+
lines.append(f"Total: {sum(len(b.operations) for b in batches)} operations, "
|
|
155
|
+
f"{total_api} API calls, ~{total_minutes} minutes")
|
|
156
|
+
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def save_plan_to_store(
|
|
161
|
+
G: nx.DiGraph,
|
|
162
|
+
store: "MigrationStore",
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Persist the execution plan (operations + edges) to SQLite.
|
|
165
|
+
|
|
166
|
+
Writes to plan_operations and plan_edges tables. Clears any
|
|
167
|
+
existing plan data first (idempotent re-planning).
|
|
168
|
+
|
|
169
|
+
(from 05-dependency-graph.md lines 300-327 — plan_operations + plan_edges tables)
|
|
170
|
+
"""
|
|
171
|
+
conn = store.conn
|
|
172
|
+
|
|
173
|
+
# Clear existing plan (edges first — FK constraint)
|
|
174
|
+
conn.execute("DELETE FROM plan_edges")
|
|
175
|
+
conn.execute("DELETE FROM plan_operations")
|
|
176
|
+
|
|
177
|
+
# Write operations
|
|
178
|
+
for node_id in G.nodes():
|
|
179
|
+
node = G.nodes[node_id]
|
|
180
|
+
payload = node.get("payload")
|
|
181
|
+
data_json = json.dumps(payload) if payload else None
|
|
182
|
+
cid = node.get("canonical_id", "")
|
|
183
|
+
# Bulk ops have synthetic canonical_ids not in the objects table.
|
|
184
|
+
# Insert a placeholder row so the FK constraint is satisfied.
|
|
185
|
+
if payload and cid:
|
|
186
|
+
conn.execute(
|
|
187
|
+
"""INSERT OR IGNORE INTO objects
|
|
188
|
+
(canonical_id, object_type, status, data, created_at, updated_at)
|
|
189
|
+
VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))""",
|
|
190
|
+
(cid, node.get("resource_type", "bulk_job"), "analyzed",
|
|
191
|
+
data_json),
|
|
192
|
+
)
|
|
193
|
+
conn.execute(
|
|
194
|
+
"""INSERT INTO plan_operations
|
|
195
|
+
(node_id, canonical_id, op_type, resource_type, tier, batch,
|
|
196
|
+
api_calls, description, status, data_json)
|
|
197
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
198
|
+
(
|
|
199
|
+
node_id,
|
|
200
|
+
node.get("canonical_id", ""),
|
|
201
|
+
node.get("op_type", ""),
|
|
202
|
+
node.get("resource_type", ""),
|
|
203
|
+
node.get("tier", 0),
|
|
204
|
+
node.get("batch"),
|
|
205
|
+
node.get("api_calls", 1),
|
|
206
|
+
node.get("description", ""),
|
|
207
|
+
"pending",
|
|
208
|
+
data_json,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Write edges
|
|
213
|
+
for u, v, data in G.edges(data=True):
|
|
214
|
+
dep_type = data.get("type", DependencyType.CONFIGURES)
|
|
215
|
+
dep_type_str = dep_type.value if hasattr(dep_type, "value") else str(dep_type)
|
|
216
|
+
conn.execute(
|
|
217
|
+
"""INSERT OR IGNORE INTO plan_edges (from_node, to_node, dep_type, broken)
|
|
218
|
+
VALUES (?, ?, ?, ?)""",
|
|
219
|
+
(u, v, dep_type_str, 0),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
conn.commit()
|
|
223
|
+
|
|
224
|
+
total_ops = G.number_of_nodes()
|
|
225
|
+
total_edges = G.number_of_edges()
|
|
226
|
+
logger.info("Saved plan to SQLite: %d operations, %d edges", total_ops, total_edges)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def load_plan_from_store(store: "MigrationStore") -> nx.DiGraph:
|
|
230
|
+
"""Reconstruct the NetworkX DiGraph from SQLite plan tables.
|
|
231
|
+
|
|
232
|
+
The tables are the source of truth; the in-memory graph is a working view.
|
|
233
|
+
|
|
234
|
+
(from 05-dependency-graph.md line 328)
|
|
235
|
+
"""
|
|
236
|
+
G = nx.DiGraph()
|
|
237
|
+
conn = store.conn
|
|
238
|
+
|
|
239
|
+
# Load nodes
|
|
240
|
+
rows = conn.execute(
|
|
241
|
+
"""SELECT node_id, canonical_id, op_type, resource_type, tier, batch,
|
|
242
|
+
api_calls, description, status
|
|
243
|
+
FROM plan_operations"""
|
|
244
|
+
).fetchall()
|
|
245
|
+
for row in rows:
|
|
246
|
+
G.add_node(
|
|
247
|
+
row["node_id"],
|
|
248
|
+
canonical_id=row["canonical_id"],
|
|
249
|
+
op_type=row["op_type"],
|
|
250
|
+
resource_type=row["resource_type"],
|
|
251
|
+
tier=row["tier"],
|
|
252
|
+
batch=row["batch"],
|
|
253
|
+
api_calls=row["api_calls"],
|
|
254
|
+
description=row["description"],
|
|
255
|
+
status=row["status"],
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Load edges
|
|
259
|
+
edge_rows = conn.execute(
|
|
260
|
+
"SELECT from_node, to_node, dep_type, broken FROM plan_edges"
|
|
261
|
+
).fetchall()
|
|
262
|
+
for row in edge_rows:
|
|
263
|
+
_valid_values = {e.value for e in DependencyType}
|
|
264
|
+
dep_type = DependencyType(row["dep_type"]) if row["dep_type"] in _valid_values else row["dep_type"]
|
|
265
|
+
G.add_edge(
|
|
266
|
+
row["from_node"],
|
|
267
|
+
row["to_node"],
|
|
268
|
+
type=dep_type,
|
|
269
|
+
broken=bool(row["broken"]),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
logger.info(
|
|
273
|
+
"Loaded plan from SQLite: %d operations, %d edges",
|
|
274
|
+
G.number_of_nodes(),
|
|
275
|
+
G.number_of_edges(),
|
|
276
|
+
)
|
|
277
|
+
return G
|