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,367 @@
|
|
|
1
|
+
"""DECT mapper: Canonical DECT networks -> enriched Webex DECT Network objects.
|
|
2
|
+
|
|
3
|
+
Reads CanonicalDECTNetwork objects produced by normalize_dect_group() (post-normalization
|
|
4
|
+
step), resolves locations via device_pool_to_location cross-refs, resolves handset owners
|
|
5
|
+
via device_owned_by_user cross-refs, merges any operator-provided base station inventory,
|
|
6
|
+
auto-selects network model (DBS-110 vs DBS-210), and generates decisions for ambiguous cases.
|
|
7
|
+
|
|
8
|
+
(from docs/superpowers/specs/2026-04-10-dect-migration.md §5c, §7a, §7b)
|
|
9
|
+
|
|
10
|
+
Cross-ref reads:
|
|
11
|
+
device_pool_to_location (DevicePool -> Location) — resolve location per network
|
|
12
|
+
device_owned_by_user (Device -> EndUser) — resolve handset owner
|
|
13
|
+
|
|
14
|
+
Decisions generated:
|
|
15
|
+
DECT_NETWORK_DESIGN — missing location, no base station inventory, or
|
|
16
|
+
multiple zones map to same location
|
|
17
|
+
DECT_HANDSET_ASSIGNMENT — handset has no owner (unowned/shared handset)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from wxcli.migration.models import (
|
|
26
|
+
CanonicalDECTNetwork,
|
|
27
|
+
DecisionType,
|
|
28
|
+
DeviceCompatibilityTier,
|
|
29
|
+
MapperResult,
|
|
30
|
+
MigrationStatus,
|
|
31
|
+
)
|
|
32
|
+
from wxcli.migration.store import MigrationStore
|
|
33
|
+
from wxcli.migration.transform.mappers.base import (
|
|
34
|
+
Mapper,
|
|
35
|
+
accept_option,
|
|
36
|
+
decision_to_store_dict,
|
|
37
|
+
extract_provenance,
|
|
38
|
+
manual_option,
|
|
39
|
+
skip_option,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
# Handset count threshold: zones with > 30 handsets require DBS-210.
|
|
45
|
+
# Zones with <= 30 handsets default to DBS-110.
|
|
46
|
+
# (from spec §3c: DBS-110 max 30 line ports; DBS-210 max 1000 line ports)
|
|
47
|
+
_DBS110_MAX_HANDSETS = 30
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DECTMapper(Mapper):
|
|
51
|
+
"""Map normalized DECT networks to enriched Webex DECT Network objects.
|
|
52
|
+
|
|
53
|
+
Runs after location_mapper (so device_pool_to_location cross-refs are set),
|
|
54
|
+
device_mapper (so DECT-tier devices exist in the store), and user_mapper
|
|
55
|
+
(so device_owned_by_user cross-refs can be resolved to canonical user IDs).
|
|
56
|
+
|
|
57
|
+
(from docs/superpowers/specs/2026-04-10-dect-migration.md §5c)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
name = "dect_mapper"
|
|
61
|
+
depends_on = ["location_mapper", "device_mapper", "user_mapper"]
|
|
62
|
+
|
|
63
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
64
|
+
"""Read CanonicalDECTNetwork objects and enrich them with resolved data."""
|
|
65
|
+
result = MapperResult()
|
|
66
|
+
|
|
67
|
+
networks = store.get_objects("dect_network")
|
|
68
|
+
if not networks:
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
# Build a map of location_canonical_id -> list of network canonical_ids
|
|
72
|
+
# to detect when multiple zones share the same location.
|
|
73
|
+
# (spec §7a: "multiple device pools with DECT handsets map to the same Webex location")
|
|
74
|
+
location_to_networks: dict[str, list[str]] = {}
|
|
75
|
+
|
|
76
|
+
for network_data in networks:
|
|
77
|
+
network_cid = network_data["canonical_id"]
|
|
78
|
+
pool_name = (network_data.get("pre_migration_state") or {}).get("cucm_device_pool", "")
|
|
79
|
+
|
|
80
|
+
# --- Location resolution ---
|
|
81
|
+
# device_pool_to_location cross-ref is written by LocationMapper.
|
|
82
|
+
# The network's pre_migration_state.cucm_device_pool is the device pool name.
|
|
83
|
+
pool_id = f"device_pool:{pool_name}" if pool_name else None
|
|
84
|
+
location_canonical_id: str | None = None
|
|
85
|
+
|
|
86
|
+
if pool_id:
|
|
87
|
+
loc_refs = store.find_cross_refs(pool_id, "device_pool_to_location")
|
|
88
|
+
location_canonical_id = loc_refs[0] if loc_refs else None
|
|
89
|
+
|
|
90
|
+
if location_canonical_id:
|
|
91
|
+
location_to_networks.setdefault(location_canonical_id, []).append(network_cid)
|
|
92
|
+
|
|
93
|
+
# Store location on the network for the second pass below
|
|
94
|
+
# (we update the network objects after the full scan)
|
|
95
|
+
network_data["_resolved_location"] = location_canonical_id
|
|
96
|
+
|
|
97
|
+
# Second pass: update each network with resolved data and generate decisions.
|
|
98
|
+
for network_data in networks:
|
|
99
|
+
network_cid = network_data["canonical_id"]
|
|
100
|
+
pool_name = (network_data.get("pre_migration_state") or {}).get("cucm_device_pool", "")
|
|
101
|
+
location_canonical_id = network_data.pop("_resolved_location", None)
|
|
102
|
+
|
|
103
|
+
handset_assignments: list[dict[str, Any]] = network_data.get("handset_assignments", [])
|
|
104
|
+
handset_count = len(handset_assignments)
|
|
105
|
+
|
|
106
|
+
# --- Auto-select network model ---
|
|
107
|
+
# DBS-110 supports up to 30 handsets / 1 base station.
|
|
108
|
+
# DBS-210 supports up to 1000 handsets / 254 base stations.
|
|
109
|
+
# (spec §5c rule 5)
|
|
110
|
+
model = "DBS-110" if handset_count <= _DBS110_MAX_HANDSETS else "DBS-210"
|
|
111
|
+
|
|
112
|
+
# --- Resolve handset owners and detect unowned/orphaned handsets ---
|
|
113
|
+
enriched_assignments: list[dict[str, Any]] = []
|
|
114
|
+
for assignment in handset_assignments:
|
|
115
|
+
device_cid = assignment.get("device_canonical_id")
|
|
116
|
+
owner_canonical_id: str | None = None
|
|
117
|
+
|
|
118
|
+
if device_cid:
|
|
119
|
+
# device_owned_by_user cross-ref source is the device canonical_id
|
|
120
|
+
owner_refs = store.find_cross_refs(device_cid, "device_owned_by_user")
|
|
121
|
+
owner_canonical_id = owner_refs[0] if owner_refs else None
|
|
122
|
+
|
|
123
|
+
enriched = dict(assignment)
|
|
124
|
+
enriched["user_canonical_id"] = owner_canonical_id
|
|
125
|
+
enriched_assignments.append(enriched)
|
|
126
|
+
|
|
127
|
+
# Look up device model and extension for decision context
|
|
128
|
+
device_model: str | None = None
|
|
129
|
+
device_extension: str | None = None
|
|
130
|
+
if device_cid:
|
|
131
|
+
device_obj = store.get_object(device_cid)
|
|
132
|
+
if device_obj:
|
|
133
|
+
device_model = device_obj.get("model")
|
|
134
|
+
# Extension from first line appearance
|
|
135
|
+
line_apps = device_obj.get("line_appearances") or []
|
|
136
|
+
if line_apps:
|
|
137
|
+
device_extension = line_apps[0].get("directory_number") or line_apps[0].get("dn")
|
|
138
|
+
|
|
139
|
+
# Determine if a DECT_HANDSET_ASSIGNMENT decision is needed:
|
|
140
|
+
# 1. Handset has no owner (unowned/shared)
|
|
141
|
+
# 2. Owner is transitioning to Webex App (no physical phone in Webex)
|
|
142
|
+
needs_assignment = False
|
|
143
|
+
owner_status = "owned"
|
|
144
|
+
|
|
145
|
+
if owner_canonical_id is None:
|
|
146
|
+
needs_assignment = True
|
|
147
|
+
owner_status = "unowned"
|
|
148
|
+
elif owner_canonical_id:
|
|
149
|
+
# Check if the owner's other devices are all WEBEX_APP tier,
|
|
150
|
+
# meaning the user is transitioning to Webex App only and
|
|
151
|
+
# won't have a DECT handset in Webex.
|
|
152
|
+
# device_owned_by_user: from=device, to=user — reverse lookup
|
|
153
|
+
owner_device_refs = store.get_cross_refs(
|
|
154
|
+
to_id=owner_canonical_id, relationship="device_owned_by_user",
|
|
155
|
+
)
|
|
156
|
+
owner_devices = [ref["from_id"] for ref in owner_device_refs]
|
|
157
|
+
# Filter to non-DECT devices owned by this user
|
|
158
|
+
non_dect_devices = []
|
|
159
|
+
for dev_id in owner_devices:
|
|
160
|
+
dev = store.get_object(dev_id)
|
|
161
|
+
if dev and dev.get("compatibility_tier") != DeviceCompatibilityTier.DECT:
|
|
162
|
+
non_dect_devices.append(dev)
|
|
163
|
+
# If all non-DECT devices are WEBEX_APP, the user is transitioning
|
|
164
|
+
if non_dect_devices and all(
|
|
165
|
+
d.get("compatibility_tier") == DeviceCompatibilityTier.WEBEX_APP
|
|
166
|
+
for d in non_dect_devices
|
|
167
|
+
):
|
|
168
|
+
needs_assignment = True
|
|
169
|
+
owner_status = "webex_app_transition"
|
|
170
|
+
|
|
171
|
+
if needs_assignment:
|
|
172
|
+
cucm_device_name = assignment.get("cucm_device_name", device_cid or "unknown")
|
|
173
|
+
summary_reason = (
|
|
174
|
+
"has no owner"
|
|
175
|
+
if owner_status == "unowned"
|
|
176
|
+
else "owner is transitioning to Webex App (no physical phone)"
|
|
177
|
+
)
|
|
178
|
+
decision = self._create_decision(
|
|
179
|
+
store=store,
|
|
180
|
+
decision_type=DecisionType.DECT_HANDSET_ASSIGNMENT,
|
|
181
|
+
severity="INFO",
|
|
182
|
+
summary=(
|
|
183
|
+
f"DECT handset '{cucm_device_name}' {summary_reason} — "
|
|
184
|
+
f"needs person or workspace assignment"
|
|
185
|
+
),
|
|
186
|
+
context={
|
|
187
|
+
"device_canonical_id": device_cid,
|
|
188
|
+
"cucm_device_name": cucm_device_name,
|
|
189
|
+
"model": device_model,
|
|
190
|
+
"extension": device_extension,
|
|
191
|
+
"device_pool": pool_name,
|
|
192
|
+
"owner_status": owner_status,
|
|
193
|
+
"network_canonical_id": network_cid,
|
|
194
|
+
},
|
|
195
|
+
options=[
|
|
196
|
+
accept_option(
|
|
197
|
+
"Create a Webex workspace for this handset and assign as PLACE"
|
|
198
|
+
),
|
|
199
|
+
manual_option(
|
|
200
|
+
"Assign to a specific person (operator provides person ID/email)"
|
|
201
|
+
),
|
|
202
|
+
skip_option("Do not migrate this handset"),
|
|
203
|
+
],
|
|
204
|
+
affected_objects=[network_cid],
|
|
205
|
+
)
|
|
206
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
207
|
+
result.decisions.append(decision)
|
|
208
|
+
|
|
209
|
+
# --- Merge operator-provided base station inventory ---
|
|
210
|
+
# Base station data lives in pre_migration_state.base_stations if provided
|
|
211
|
+
# via --dect-inventory flag on discover. If absent, base_stations stays empty.
|
|
212
|
+
base_stations: list[dict[str, Any]] = (
|
|
213
|
+
(network_data.get("pre_migration_state") or {}).get("base_stations", [])
|
|
214
|
+
or network_data.get("base_stations", [])
|
|
215
|
+
)
|
|
216
|
+
has_inventory = bool(base_stations)
|
|
217
|
+
|
|
218
|
+
# --- Generate DECT_NETWORK_DESIGN decisions for ambiguous cases ---
|
|
219
|
+
|
|
220
|
+
# Case 1: No location resolved
|
|
221
|
+
if location_canonical_id is None:
|
|
222
|
+
decision = self._create_decision(
|
|
223
|
+
store=store,
|
|
224
|
+
decision_type=DecisionType.DECT_NETWORK_DESIGN,
|
|
225
|
+
severity="HIGH",
|
|
226
|
+
summary=(
|
|
227
|
+
f"DECT network for pool '{pool_name}' could not resolve a "
|
|
228
|
+
f"target Webex location — manual assignment required"
|
|
229
|
+
),
|
|
230
|
+
context={
|
|
231
|
+
"network_canonical_id": network_cid,
|
|
232
|
+
"cucm_device_pool": pool_name,
|
|
233
|
+
"total_handsets": handset_count,
|
|
234
|
+
"base_stations_provided": has_inventory,
|
|
235
|
+
"location_name": None,
|
|
236
|
+
"zone_count": 1,
|
|
237
|
+
"handsets_per_zone": {pool_name: handset_count},
|
|
238
|
+
},
|
|
239
|
+
options=[
|
|
240
|
+
manual_option(
|
|
241
|
+
"Assign this DECT network to a Webex location and re-run the mapper"
|
|
242
|
+
),
|
|
243
|
+
skip_option("Exclude this DECT network from migration"),
|
|
244
|
+
],
|
|
245
|
+
affected_objects=[network_cid],
|
|
246
|
+
)
|
|
247
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
248
|
+
result.decisions.append(decision)
|
|
249
|
+
|
|
250
|
+
# Case 2: No base station inventory provided
|
|
251
|
+
if not has_inventory:
|
|
252
|
+
decision = self._create_decision(
|
|
253
|
+
store=store,
|
|
254
|
+
decision_type=DecisionType.DECT_NETWORK_DESIGN,
|
|
255
|
+
severity="MEDIUM",
|
|
256
|
+
summary=(
|
|
257
|
+
f"DECT network for pool '{pool_name}' has no base station inventory — "
|
|
258
|
+
f"provide MAC addresses to enable automated provisioning"
|
|
259
|
+
),
|
|
260
|
+
context={
|
|
261
|
+
"network_canonical_id": network_cid,
|
|
262
|
+
"cucm_device_pool": pool_name,
|
|
263
|
+
"total_handsets": handset_count,
|
|
264
|
+
"base_stations_provided": False,
|
|
265
|
+
"location_name": location_canonical_id,
|
|
266
|
+
"zone_count": 1,
|
|
267
|
+
"handsets_per_zone": {pool_name: handset_count},
|
|
268
|
+
},
|
|
269
|
+
options=[
|
|
270
|
+
accept_option(
|
|
271
|
+
f"Create one {'DBS-210' if handset_count > _DBS110_MAX_HANDSETS else 'DBS-110'} "
|
|
272
|
+
f"DECT network for this zone — base stations must be added manually in Control Hub"
|
|
273
|
+
),
|
|
274
|
+
manual_option(
|
|
275
|
+
"Provide base station inventory CSV (--dect-inventory) and re-run"
|
|
276
|
+
),
|
|
277
|
+
skip_option("Operator will design DECT networks manually in Control Hub"),
|
|
278
|
+
],
|
|
279
|
+
affected_objects=[network_cid],
|
|
280
|
+
)
|
|
281
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
282
|
+
result.decisions.append(decision)
|
|
283
|
+
|
|
284
|
+
# Case 3: Multiple zones map to the same location (detected via location_to_networks)
|
|
285
|
+
if location_canonical_id and len(location_to_networks.get(location_canonical_id, [])) > 1:
|
|
286
|
+
# Only emit this decision for the first network in the location group to avoid
|
|
287
|
+
# N duplicate decisions. The first network is the canonical_id at index 0.
|
|
288
|
+
zone_group = location_to_networks[location_canonical_id]
|
|
289
|
+
if zone_group[0] == network_cid:
|
|
290
|
+
# Collect per-zone handset counts for all networks in this location
|
|
291
|
+
handsets_per_zone: dict[str, int] = {}
|
|
292
|
+
total_in_loc = 0
|
|
293
|
+
for sibling_cid in zone_group:
|
|
294
|
+
sibling_data = next(
|
|
295
|
+
(n for n in networks if n["canonical_id"] == sibling_cid), {}
|
|
296
|
+
)
|
|
297
|
+
sibling_pool = (sibling_data.get("pre_migration_state") or {}).get(
|
|
298
|
+
"cucm_device_pool", sibling_cid
|
|
299
|
+
)
|
|
300
|
+
sibling_count = len(sibling_data.get("handset_assignments", []))
|
|
301
|
+
handsets_per_zone[sibling_pool] = sibling_count
|
|
302
|
+
total_in_loc += sibling_count
|
|
303
|
+
|
|
304
|
+
decision = self._create_decision(
|
|
305
|
+
store=store,
|
|
306
|
+
decision_type=DecisionType.DECT_NETWORK_DESIGN,
|
|
307
|
+
severity="MEDIUM",
|
|
308
|
+
summary=(
|
|
309
|
+
f"Multiple DECT coverage zones ({len(zone_group)}) map to the same "
|
|
310
|
+
f"Webex location '{location_canonical_id}' — "
|
|
311
|
+
f"decide: one combined network or separate networks per zone"
|
|
312
|
+
),
|
|
313
|
+
context={
|
|
314
|
+
"network_canonical_id": network_cid,
|
|
315
|
+
"location_name": location_canonical_id,
|
|
316
|
+
"zone_count": len(zone_group),
|
|
317
|
+
"total_handsets": total_in_loc,
|
|
318
|
+
"handsets_per_zone": handsets_per_zone,
|
|
319
|
+
"base_stations_provided": has_inventory,
|
|
320
|
+
},
|
|
321
|
+
options=[
|
|
322
|
+
accept_option(
|
|
323
|
+
"Create one DBS-210 multi-cell network for the location — "
|
|
324
|
+
"all base stations and handsets go into this network"
|
|
325
|
+
),
|
|
326
|
+
manual_option(
|
|
327
|
+
"Create separate DECT networks per coverage zone — "
|
|
328
|
+
"provide zone-to-base-station mapping"
|
|
329
|
+
),
|
|
330
|
+
skip_option(
|
|
331
|
+
"Operator will design DECT networks manually in Control Hub "
|
|
332
|
+
"after migration"
|
|
333
|
+
),
|
|
334
|
+
],
|
|
335
|
+
affected_objects=zone_group,
|
|
336
|
+
)
|
|
337
|
+
store.save_decision(decision_to_store_dict(decision))
|
|
338
|
+
result.decisions.append(decision)
|
|
339
|
+
|
|
340
|
+
# --- Build and upsert the enriched CanonicalDECTNetwork ---
|
|
341
|
+
provenance = extract_provenance(network_data)
|
|
342
|
+
# Webex DECT display_name max 11 chars (shown on handset screen)
|
|
343
|
+
raw_display = network_data.get("display_name") or network_data.get("network_name") or f"DECT-{pool_name}"
|
|
344
|
+
display_name = raw_display[:11] if raw_display else None
|
|
345
|
+
enriched_network = CanonicalDECTNetwork(
|
|
346
|
+
canonical_id=network_cid,
|
|
347
|
+
provenance=provenance,
|
|
348
|
+
status=MigrationStatus.ANALYZED,
|
|
349
|
+
location_canonical_id=location_canonical_id,
|
|
350
|
+
network_name=network_data.get("network_name") or f"DECT-{pool_name}",
|
|
351
|
+
display_name=display_name,
|
|
352
|
+
model=model,
|
|
353
|
+
access_code=network_data.get("access_code") or "",
|
|
354
|
+
base_stations=base_stations,
|
|
355
|
+
handset_assignments=enriched_assignments,
|
|
356
|
+
pre_migration_state={
|
|
357
|
+
**(network_data.get("pre_migration_state") or {}),
|
|
358
|
+
"handset_count": handset_count,
|
|
359
|
+
"has_base_station_inventory": has_inventory,
|
|
360
|
+
"auto_selected_model": model,
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
store.upsert_object(enriched_network)
|
|
365
|
+
result.objects_created += 1
|
|
366
|
+
|
|
367
|
+
return result
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""DeviceLayoutMapper: per-device resolved line key + KEM layout.
|
|
2
|
+
|
|
3
|
+
Merges CanonicalLineKeyTemplate structure with per-phone line appearances,
|
|
4
|
+
speed dials, and BLF entries. Resolves DNs to canonical user IDs and
|
|
5
|
+
detects shared lines via CanonicalSharedLine objects.
|
|
6
|
+
|
|
7
|
+
Reads raw phone objects (object_type="phone") which contain the full AXL
|
|
8
|
+
getPhone response in pre_migration_state. Key fields:
|
|
9
|
+
- lines: list of line dicts with {index, label, dirn: {pattern, routePartitionName}}
|
|
10
|
+
- speeddials: {speeddial: [{dirn, label, index}, ...]}
|
|
11
|
+
- busyLampFields: {busyLampField: [{blfDest, label, index}, ...]}
|
|
12
|
+
- ownerUserName: zeep ref dict {"_value_1": "userid", "uuid": "..."}
|
|
13
|
+
|
|
14
|
+
(from tier2-phase2-phone-config-design.md §4.2)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from wxcli.migration.models import (
|
|
22
|
+
CanonicalDeviceLayout,
|
|
23
|
+
MapperResult,
|
|
24
|
+
MigrationStatus,
|
|
25
|
+
)
|
|
26
|
+
from wxcli.migration.store import MigrationStore
|
|
27
|
+
from wxcli.migration.transform.mappers.base import (
|
|
28
|
+
Mapper,
|
|
29
|
+
extract_provenance,
|
|
30
|
+
)
|
|
31
|
+
from wxcli.migration.transform.mappers.monitoring_mapper import (
|
|
32
|
+
_extract_blf_entries,
|
|
33
|
+
_extract_speed_dials,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ref_value(field: Any) -> str | None:
|
|
40
|
+
"""Extract display value from a zeep ref dict or plain string."""
|
|
41
|
+
if field is None:
|
|
42
|
+
return None
|
|
43
|
+
if isinstance(field, str):
|
|
44
|
+
return field or None
|
|
45
|
+
if isinstance(field, dict):
|
|
46
|
+
val = field.get("_value_1")
|
|
47
|
+
return val if val else None
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DeviceLayoutMapper(Mapper):
|
|
52
|
+
"""Per-device layout: merges button template with actual phone data."""
|
|
53
|
+
|
|
54
|
+
name = "device_layout_mapper"
|
|
55
|
+
depends_on = ["button_template_mapper", "monitoring_mapper", "line_mapper", "device_mapper"]
|
|
56
|
+
|
|
57
|
+
def map(self, store: MigrationStore) -> MapperResult:
|
|
58
|
+
result = MapperResult()
|
|
59
|
+
|
|
60
|
+
shared_dns = self._build_shared_dn_set(store)
|
|
61
|
+
|
|
62
|
+
for phone_data in store.get_objects("phone"):
|
|
63
|
+
phone_id = phone_data["canonical_id"]
|
|
64
|
+
state = phone_data.get("pre_migration_state") or {}
|
|
65
|
+
|
|
66
|
+
# Skip common-area phones (no ownerUserName)
|
|
67
|
+
owner_raw = _ref_value(state.get("ownerUserName"))
|
|
68
|
+
if not owner_raw and state.get("class") == "Phone":
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
phone_name = state.get("name", phone_id.split(":", 1)[-1])
|
|
72
|
+
device_id = f"device:{phone_name}"
|
|
73
|
+
|
|
74
|
+
# Resolve owner via device cross-ref (device_owned_by_user links device:, not phone:)
|
|
75
|
+
owner_id = None
|
|
76
|
+
owner_refs = store.find_cross_refs(device_id, "device_owned_by_user")
|
|
77
|
+
if owner_refs:
|
|
78
|
+
owner_id = owner_refs[0]
|
|
79
|
+
|
|
80
|
+
# Resolve button template via phone cross-ref
|
|
81
|
+
tmpl_refs = store.find_cross_refs(phone_id, "phone_uses_button_template")
|
|
82
|
+
tmpl_id = None
|
|
83
|
+
tmpl_line_keys: list[dict] = []
|
|
84
|
+
tmpl_kem_keys: list[dict] = []
|
|
85
|
+
if tmpl_refs:
|
|
86
|
+
lkt_id = tmpl_refs[0].replace("button_template:", "line_key_template:")
|
|
87
|
+
lkt = store.get_object(lkt_id)
|
|
88
|
+
if lkt:
|
|
89
|
+
tmpl_id = lkt_id
|
|
90
|
+
lkt_data = lkt if isinstance(lkt, dict) else lkt.model_dump()
|
|
91
|
+
tmpl_line_keys = lkt_data.get("line_keys", [])
|
|
92
|
+
tmpl_kem_keys = lkt_data.get("kem_keys", [])
|
|
93
|
+
|
|
94
|
+
# Raw phone data: lines are a flat list from the extractor
|
|
95
|
+
raw_lines = state.get("lines", [])
|
|
96
|
+
if isinstance(raw_lines, dict):
|
|
97
|
+
raw_lines = raw_lines.get("line", [])
|
|
98
|
+
if isinstance(raw_lines, dict):
|
|
99
|
+
raw_lines = [raw_lines]
|
|
100
|
+
|
|
101
|
+
# Index line appearances by position
|
|
102
|
+
la_by_index: dict[int, dict] = {}
|
|
103
|
+
for la in raw_lines:
|
|
104
|
+
if not isinstance(la, dict):
|
|
105
|
+
continue
|
|
106
|
+
dirn = la.get("dirn")
|
|
107
|
+
if not dirn or not isinstance(dirn, dict):
|
|
108
|
+
continue
|
|
109
|
+
idx = la.get("index")
|
|
110
|
+
if idx is not None:
|
|
111
|
+
la_by_index[int(idx)] = la
|
|
112
|
+
|
|
113
|
+
# Speed dials and BLF indexed by button position
|
|
114
|
+
speed_dials_raw = _extract_speed_dials(state)
|
|
115
|
+
blf_entries_raw = _extract_blf_entries(state)
|
|
116
|
+
|
|
117
|
+
sd_by_index: dict[int, dict] = {}
|
|
118
|
+
for sd in speed_dials_raw:
|
|
119
|
+
idx = sd.get("index") or sd.get("speedDialIndex")
|
|
120
|
+
if idx is not None:
|
|
121
|
+
sd_by_index[int(idx)] = sd
|
|
122
|
+
|
|
123
|
+
blf_by_index: dict[int, dict] = {}
|
|
124
|
+
for blf in blf_entries_raw:
|
|
125
|
+
idx = blf.get("index")
|
|
126
|
+
if idx is not None:
|
|
127
|
+
blf_by_index[int(idx)] = blf
|
|
128
|
+
|
|
129
|
+
# Build line_members from line appearances
|
|
130
|
+
line_members: list[dict[str, Any]] = []
|
|
131
|
+
for la_idx in sorted(la_by_index.keys()):
|
|
132
|
+
la = la_by_index[la_idx]
|
|
133
|
+
dirn = la.get("dirn", {})
|
|
134
|
+
dn = dirn.get("pattern", "")
|
|
135
|
+
partition = _ref_value(dirn.get("routePartitionName"))
|
|
136
|
+
label = la.get("label")
|
|
137
|
+
|
|
138
|
+
member_id = self._resolve_dn_to_user(store, dn)
|
|
139
|
+
|
|
140
|
+
dn_key = f"{dn}:{partition}" if partition else dn
|
|
141
|
+
line_type = (
|
|
142
|
+
"SHARED_LINE"
|
|
143
|
+
if dn_key in shared_dns or dn in shared_dns
|
|
144
|
+
else "PRIMARY"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
line_members.append({
|
|
148
|
+
"port": la_idx,
|
|
149
|
+
"member_canonical_id": member_id,
|
|
150
|
+
"line_type": line_type,
|
|
151
|
+
"line_label": label,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
# Merge template keys with phone data
|
|
155
|
+
resolved_line_keys = self._resolve_keys(
|
|
156
|
+
tmpl_line_keys, la_by_index, sd_by_index, blf_by_index, store,
|
|
157
|
+
)
|
|
158
|
+
resolved_kem_keys = self._resolve_keys(
|
|
159
|
+
tmpl_kem_keys, la_by_index, sd_by_index, blf_by_index, store,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Collect speed dials
|
|
163
|
+
speed_dials = []
|
|
164
|
+
for idx in sorted(sd_by_index.keys()):
|
|
165
|
+
sd = sd_by_index[idx]
|
|
166
|
+
speed_dials.append({
|
|
167
|
+
"index": idx,
|
|
168
|
+
"label": sd.get("label", ""),
|
|
169
|
+
"number": sd.get("dirn") or sd.get("speedDialNumber", ""),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
# Skip CONVERTIBLE devices — they use activation-code registration
|
|
173
|
+
# and configure themselves post-registration. Layout application for
|
|
174
|
+
# these phones is deferred to a post-registration bulk job.
|
|
175
|
+
_skip_device_obj = store.get_object(device_id)
|
|
176
|
+
if _skip_device_obj and _skip_device_obj.get("compatibility_tier") == "convertible":
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Copy device_id_surface from the associated CanonicalDevice
|
|
180
|
+
device_obj = store.get_object(device_id)
|
|
181
|
+
device_id_surface = device_obj.get("device_id_surface", "telephony") if device_obj else "telephony"
|
|
182
|
+
|
|
183
|
+
layout = CanonicalDeviceLayout(
|
|
184
|
+
canonical_id=f"device_layout:{phone_name}",
|
|
185
|
+
provenance=extract_provenance(phone_data),
|
|
186
|
+
status=MigrationStatus.ANALYZED,
|
|
187
|
+
device_canonical_id=device_id,
|
|
188
|
+
template_canonical_id=tmpl_id,
|
|
189
|
+
owner_canonical_id=owner_id,
|
|
190
|
+
line_members=line_members,
|
|
191
|
+
resolved_line_keys=resolved_line_keys,
|
|
192
|
+
resolved_kem_keys=resolved_kem_keys,
|
|
193
|
+
speed_dials=speed_dials,
|
|
194
|
+
device_id_surface=device_id_surface,
|
|
195
|
+
)
|
|
196
|
+
store.upsert_object(layout)
|
|
197
|
+
|
|
198
|
+
store.add_cross_ref(phone_id, layout.canonical_id, "phone_has_layout")
|
|
199
|
+
if device_obj is not None:
|
|
200
|
+
store.add_cross_ref(device_id, layout.canonical_id, "device_has_layout")
|
|
201
|
+
if layout.template_canonical_id:
|
|
202
|
+
store.add_cross_ref(
|
|
203
|
+
layout.canonical_id,
|
|
204
|
+
layout.template_canonical_id,
|
|
205
|
+
"layout_uses_template",
|
|
206
|
+
)
|
|
207
|
+
result.objects_created += 1
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# Private helpers
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def _resolve_keys(
|
|
216
|
+
self,
|
|
217
|
+
template_keys: list[dict],
|
|
218
|
+
la_by_index: dict[int, dict],
|
|
219
|
+
sd_by_index: dict[int, dict],
|
|
220
|
+
blf_by_index: dict[int, dict],
|
|
221
|
+
store: MigrationStore,
|
|
222
|
+
) -> list[dict[str, Any]]:
|
|
223
|
+
"""Resolve template key slots with phone-specific values."""
|
|
224
|
+
resolved = []
|
|
225
|
+
for key in template_keys:
|
|
226
|
+
idx = key.get("index", 0)
|
|
227
|
+
key_type = key.get("key_type", "OPEN")
|
|
228
|
+
entry: dict[str, Any] = {"index": idx, "key_type": key_type}
|
|
229
|
+
|
|
230
|
+
if key_type in ("PRIMARY_LINE", "SHARED_LINE", "LINE"):
|
|
231
|
+
la = la_by_index.get(idx)
|
|
232
|
+
if la:
|
|
233
|
+
dirn = la.get("dirn", {})
|
|
234
|
+
dn = dirn.get("pattern", "") if isinstance(dirn, dict) else ""
|
|
235
|
+
entry["label"] = la.get("label")
|
|
236
|
+
entry["target_canonical_id"] = self._resolve_dn_to_user(store, dn)
|
|
237
|
+
|
|
238
|
+
elif key_type == "SPEED_DIAL":
|
|
239
|
+
sd = sd_by_index.get(idx)
|
|
240
|
+
if sd:
|
|
241
|
+
entry["label"] = sd.get("label", "")
|
|
242
|
+
entry["value"] = sd.get("dirn") or sd.get("speedDialNumber", "")
|
|
243
|
+
|
|
244
|
+
elif key_type == "MONITOR":
|
|
245
|
+
blf = blf_by_index.get(idx)
|
|
246
|
+
if blf:
|
|
247
|
+
target_dn = blf.get("blfDest") or ""
|
|
248
|
+
entry["label"] = blf.get("label", target_dn)
|
|
249
|
+
entry["target_canonical_id"] = self._resolve_dn_to_user(
|
|
250
|
+
store, target_dn,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
elif key_type == "CALL_PARK_EXTENSION":
|
|
254
|
+
entry["label"] = "Call Park"
|
|
255
|
+
|
|
256
|
+
resolved.append(entry)
|
|
257
|
+
return resolved
|
|
258
|
+
|
|
259
|
+
def _resolve_dn_to_user(
|
|
260
|
+
self, store: MigrationStore, dn: str,
|
|
261
|
+
) -> str | None:
|
|
262
|
+
"""Resolve a DN to a user canonical_id via line cross-refs."""
|
|
263
|
+
if not dn:
|
|
264
|
+
return None
|
|
265
|
+
for line_data in store.get_objects("line"):
|
|
266
|
+
line_state = line_data.get("pre_migration_state") or {}
|
|
267
|
+
line_ext = (
|
|
268
|
+
line_data.get("extension")
|
|
269
|
+
or line_state.get("extension")
|
|
270
|
+
or line_state.get("pattern")
|
|
271
|
+
)
|
|
272
|
+
if line_ext == dn:
|
|
273
|
+
line_id = line_data["canonical_id"]
|
|
274
|
+
user_refs = store.find_cross_refs(line_id, "line_assigned_to_user")
|
|
275
|
+
if user_refs:
|
|
276
|
+
return user_refs[0]
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
def _build_shared_dn_set(self, store: MigrationStore) -> set[str]:
|
|
280
|
+
"""Build a set of DN keys that are shared lines."""
|
|
281
|
+
shared: set[str] = set()
|
|
282
|
+
for sl in store.get_objects("shared_line"):
|
|
283
|
+
dn_id = sl.get("dn_canonical_id", "")
|
|
284
|
+
if dn_id:
|
|
285
|
+
parts = dn_id.split(":", 1)
|
|
286
|
+
if len(parts) > 1:
|
|
287
|
+
shared.add(parts[1])
|
|
288
|
+
shared.add(dn_id)
|
|
289
|
+
return shared
|