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,293 @@
|
|
|
1
|
+
"""Preflight runner — orchestrates all checks and manages shared data.
|
|
2
|
+
|
|
3
|
+
Fetches shared Webex data once (licenses, locations, numbers, people, trunks),
|
|
4
|
+
runs all checks, merges decisions, and stores results in state.json.
|
|
5
|
+
|
|
6
|
+
(from 05a-preflight-checks.md, PreflightRunner class)
|
|
7
|
+
(from phase-10-preflight.md, runner orchestrator)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from wxcli.migration.preflight import (
|
|
17
|
+
CheckResult,
|
|
18
|
+
CheckStatus,
|
|
19
|
+
PreflightError,
|
|
20
|
+
PreflightResult,
|
|
21
|
+
_run_wxcli,
|
|
22
|
+
)
|
|
23
|
+
from wxcli.migration.preflight.checks import (
|
|
24
|
+
check_bulk_device_job_support,
|
|
25
|
+
check_duplicate_users,
|
|
26
|
+
check_e911_readiness,
|
|
27
|
+
check_feature_entitlements,
|
|
28
|
+
check_licenses,
|
|
29
|
+
check_locations,
|
|
30
|
+
check_number_conflicts,
|
|
31
|
+
check_rate_limit_budget,
|
|
32
|
+
check_trunks,
|
|
33
|
+
check_workspace_licenses,
|
|
34
|
+
)
|
|
35
|
+
from wxcli.migration.store import MigrationStore
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Priority order for overall result (worst wins)
|
|
39
|
+
_STATUS_PRIORITY = {
|
|
40
|
+
CheckStatus.FAIL: 3,
|
|
41
|
+
CheckStatus.WARN: 2,
|
|
42
|
+
CheckStatus.PASS: 1,
|
|
43
|
+
CheckStatus.SKIP: 0,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _worst_status(results: list[CheckResult]) -> CheckStatus:
|
|
48
|
+
"""Return the worst status across all results."""
|
|
49
|
+
worst = CheckStatus.SKIP
|
|
50
|
+
for r in results:
|
|
51
|
+
if _STATUS_PRIORITY.get(r.status, 0) > _STATUS_PRIORITY.get(worst, 0):
|
|
52
|
+
worst = r.status
|
|
53
|
+
return worst
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PreflightRunner:
|
|
57
|
+
"""Orchestrates all 7 preflight checks + DUPLICATE_USER detection.
|
|
58
|
+
|
|
59
|
+
Fetches shared data once before running individual checks.
|
|
60
|
+
(from 05a-preflight-checks.md, Shared Data Between Checks)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, config: dict | None = None):
|
|
64
|
+
self.config = config or {}
|
|
65
|
+
|
|
66
|
+
def run(
|
|
67
|
+
self,
|
|
68
|
+
store: MigrationStore,
|
|
69
|
+
check_filter: str | None = None,
|
|
70
|
+
dry_run: bool = False,
|
|
71
|
+
) -> PreflightResult:
|
|
72
|
+
"""Run all preflight checks (or a single check by name).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
store: Migration store with planned objects.
|
|
76
|
+
check_filter: Run only this check (e.g., "numbers", "licenses").
|
|
77
|
+
dry_run: Show what would be checked without querying Webex.
|
|
78
|
+
"""
|
|
79
|
+
if dry_run:
|
|
80
|
+
return self._dry_run(store, check_filter)
|
|
81
|
+
|
|
82
|
+
# Fetch shared data — only what's needed for the filtered check
|
|
83
|
+
# (from 05a-preflight-checks.md, Shared Data Between Checks)
|
|
84
|
+
def _needs(*checks: str) -> bool:
|
|
85
|
+
return check_filter is None or check_filter in checks
|
|
86
|
+
|
|
87
|
+
licenses = self._fetch("licenses", ["licenses-api", "list", "--output", "json"]) if _needs("licenses", "workspace-licenses") else []
|
|
88
|
+
locations = self._fetch("locations", ["locations", "list"]) if _needs("locations") else []
|
|
89
|
+
numbers = self._fetch("numbers", ["numbers", "list", "--limit", "0"]) if _needs("numbers") else []
|
|
90
|
+
people = self._fetch("people", ["users", "list", "--calling-data", "true", "--limit", "0"]) if _needs("users") else []
|
|
91
|
+
trunks = self._fetch("trunks", ["call-routing", "list-trunks"]) if _needs("trunks") else []
|
|
92
|
+
|
|
93
|
+
# Fetch PSTN connection data per location
|
|
94
|
+
pstn_by_loc: dict[str, dict] = {}
|
|
95
|
+
if locations and _needs("locations"):
|
|
96
|
+
for loc in locations:
|
|
97
|
+
loc_id = loc.get("id")
|
|
98
|
+
if loc_id:
|
|
99
|
+
conn = self._fetch(f"pstn-{loc_id}",
|
|
100
|
+
["pstn", "list-connection", loc_id, "--output", "json"])
|
|
101
|
+
if conn:
|
|
102
|
+
pstn_by_loc[loc_id] = conn[0] if isinstance(conn, list) and conn else conn if isinstance(conn, dict) else {}
|
|
103
|
+
|
|
104
|
+
existing_features: dict[str, int] = {}
|
|
105
|
+
if _needs("features"):
|
|
106
|
+
existing_features = self._count_existing_features()
|
|
107
|
+
|
|
108
|
+
# Build bulk-job probe only when the plan contains bulk ops
|
|
109
|
+
# (avoids initialising a WebexSimpleApi session otherwise).
|
|
110
|
+
probe_fn = None
|
|
111
|
+
if _needs("bulk-job-support"):
|
|
112
|
+
probe_fn = self._build_bulk_job_probe()
|
|
113
|
+
|
|
114
|
+
# Run checks
|
|
115
|
+
all_checks = {
|
|
116
|
+
"licenses": lambda: check_licenses(store, licenses),
|
|
117
|
+
"workspace-licenses": lambda: check_workspace_licenses(store, licenses),
|
|
118
|
+
"locations": lambda: check_locations(store, locations, pstn_connections=pstn_by_loc if locations else None),
|
|
119
|
+
"trunks": lambda: check_trunks(store, trunks),
|
|
120
|
+
"features": lambda: check_feature_entitlements(store, existing_features),
|
|
121
|
+
"numbers": lambda: self._run_number_check(store, numbers),
|
|
122
|
+
"users": lambda: self._run_duplicate_check(store, people),
|
|
123
|
+
"rate-limit": lambda: check_rate_limit_budget(store, self.config),
|
|
124
|
+
"e911-readiness": lambda: check_e911_readiness(store),
|
|
125
|
+
"bulk-job-support": lambda: check_bulk_device_job_support(store, probe_fn),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
results: list[CheckResult] = []
|
|
129
|
+
all_decisions: list[dict] = []
|
|
130
|
+
self._number_decisions: list[dict] = []
|
|
131
|
+
self._user_decisions: list[dict] = []
|
|
132
|
+
|
|
133
|
+
if check_filter:
|
|
134
|
+
if check_filter not in all_checks:
|
|
135
|
+
valid = ", ".join(sorted(all_checks.keys()))
|
|
136
|
+
raise PreflightError(f"Unknown check '{check_filter}'. Valid: {valid}")
|
|
137
|
+
result = all_checks[check_filter]()
|
|
138
|
+
results.append(result)
|
|
139
|
+
else:
|
|
140
|
+
for name, check_fn in all_checks.items():
|
|
141
|
+
try:
|
|
142
|
+
result = check_fn()
|
|
143
|
+
results.append(result)
|
|
144
|
+
except PreflightError as e:
|
|
145
|
+
results.append(CheckResult(
|
|
146
|
+
name=name,
|
|
147
|
+
status=CheckStatus.FAIL,
|
|
148
|
+
detail=str(e),
|
|
149
|
+
))
|
|
150
|
+
|
|
151
|
+
all_decisions = self._number_decisions + self._user_decisions
|
|
152
|
+
|
|
153
|
+
# Merge decisions into store — scoped to preflight types only
|
|
154
|
+
# to avoid stale-marking analyzer decisions
|
|
155
|
+
# (from 05a-preflight-checks.md lines 920-929)
|
|
156
|
+
merge_result: dict[str, int] = {}
|
|
157
|
+
new_decision_ids: list[str] = []
|
|
158
|
+
if all_decisions:
|
|
159
|
+
merge_result = store.merge_decisions(
|
|
160
|
+
all_decisions,
|
|
161
|
+
decision_types=["NUMBER_CONFLICT", "DUPLICATE_USER"],
|
|
162
|
+
stage="preflight",
|
|
163
|
+
)
|
|
164
|
+
new_decision_ids = [d["decision_id"] for d in all_decisions]
|
|
165
|
+
|
|
166
|
+
overall = _worst_status(results)
|
|
167
|
+
|
|
168
|
+
return PreflightResult(
|
|
169
|
+
overall=overall,
|
|
170
|
+
checks=results,
|
|
171
|
+
new_decision_ids=new_decision_ids,
|
|
172
|
+
merge_result=merge_result,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _run_number_check(self, store: MigrationStore, numbers: list[dict]) -> CheckResult:
|
|
176
|
+
"""Run number conflict check and capture decisions."""
|
|
177
|
+
result, decisions = check_number_conflicts(store, numbers)
|
|
178
|
+
self._number_decisions = decisions
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def _run_duplicate_check(self, store: MigrationStore, people: list[dict]) -> CheckResult:
|
|
182
|
+
"""Run duplicate user check and capture decisions."""
|
|
183
|
+
result, decisions = check_duplicate_users(store, people)
|
|
184
|
+
self._user_decisions = decisions
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def _fetch(self, label: str, args: list[str]) -> list[dict]:
|
|
188
|
+
"""Fetch data from wxcli, returning empty list on failure."""
|
|
189
|
+
try:
|
|
190
|
+
return _run_wxcli(args)
|
|
191
|
+
except PreflightError:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
def _build_bulk_job_probe(self):
|
|
195
|
+
"""Return a zero-arg callable that probes the bulk device job endpoint.
|
|
196
|
+
|
|
197
|
+
Returns ``None`` if auth is unavailable — the check then SKIPs.
|
|
198
|
+
On invocation the callable returns ``(status_code, error_message)``.
|
|
199
|
+
Uses ``WebexSimpleApi`` so auth/orgId injection matches the rest of
|
|
200
|
+
the CLI.
|
|
201
|
+
|
|
202
|
+
Design note (Finding #11): this preflight module's convention is
|
|
203
|
+
**subprocess, not import** — checks normally shell out to ``wxcli``
|
|
204
|
+
via ``_run_wxcli`` to reuse the CLI's auth, pagination, and error
|
|
205
|
+
handling (see the module CLAUDE.md). This probe deliberately deviates
|
|
206
|
+
and calls ``api.session.get`` directly because there is no equivalent
|
|
207
|
+
``wxcli`` subcommand that lists bulk device jobs — generating one
|
|
208
|
+
just to satisfy the convention would be overkill for a single
|
|
209
|
+
read-only probe. If ``wxcli`` ever gains a
|
|
210
|
+
``bulk-device-jobs list --job-type callDeviceSettings --max 1``
|
|
211
|
+
command, this probe should be re-routed through ``_run_wxcli`` to
|
|
212
|
+
restore convention parity.
|
|
213
|
+
(Wave 4, Issue #9)
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
from wxcli.auth import get_api
|
|
217
|
+
except Exception:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
api = get_api()
|
|
222
|
+
except SystemExit:
|
|
223
|
+
# get_api raises typer.Exit when no token is configured
|
|
224
|
+
return None
|
|
225
|
+
except Exception:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _probe() -> tuple[int, str]:
|
|
229
|
+
import requests
|
|
230
|
+
|
|
231
|
+
url = api.session.ep("telephony/config/jobs/devices/callDeviceSettings")
|
|
232
|
+
params: dict[str, str] = {"max": "1"}
|
|
233
|
+
org_id = self.config.get("orgId") if isinstance(self.config, dict) else None
|
|
234
|
+
if org_id:
|
|
235
|
+
params["orgId"] = org_id
|
|
236
|
+
try:
|
|
237
|
+
resp = api.session.get(url, params=params)
|
|
238
|
+
except requests.RequestException as exc:
|
|
239
|
+
return 0, str(exc)
|
|
240
|
+
return resp.status_code, getattr(resp, "text", "") or ""
|
|
241
|
+
|
|
242
|
+
return _probe
|
|
243
|
+
|
|
244
|
+
def _count_existing_features(self) -> dict[str, int]:
|
|
245
|
+
"""Count existing features in the Webex org."""
|
|
246
|
+
counts = {}
|
|
247
|
+
cmd_map = {
|
|
248
|
+
"auto_attendant": ["auto-attendant", "list"],
|
|
249
|
+
"call_queue": ["call-queue", "list"],
|
|
250
|
+
"hunt_group": ["hunt-group", "list"],
|
|
251
|
+
"paging_group": ["paging-group", "list"],
|
|
252
|
+
}
|
|
253
|
+
for obj_type, args in cmd_map.items():
|
|
254
|
+
try:
|
|
255
|
+
data = _run_wxcli(args)
|
|
256
|
+
counts[obj_type] = len(data)
|
|
257
|
+
except PreflightError:
|
|
258
|
+
counts[obj_type] = 0
|
|
259
|
+
return counts
|
|
260
|
+
|
|
261
|
+
def _dry_run(
|
|
262
|
+
self,
|
|
263
|
+
store: MigrationStore,
|
|
264
|
+
check_filter: str | None,
|
|
265
|
+
) -> PreflightResult:
|
|
266
|
+
"""Show what would be checked without querying Webex."""
|
|
267
|
+
checks_info = [
|
|
268
|
+
("licenses", "User licenses", f"{store.count_by_type('user')} users to check"),
|
|
269
|
+
("workspace-licenses", "Workspace licenses", f"{store.count_by_type('workspace')} workspaces to check"),
|
|
270
|
+
("locations", "Locations", f"{len(store.get_objects('location'))} locations to verify"),
|
|
271
|
+
("trunks", "Trunks", f"{len(store.get_objects('trunk'))} trunks to verify"),
|
|
272
|
+
("features", "Feature entitlements", "AA, CQ, HG, Paging counts"),
|
|
273
|
+
("numbers", "Number conflicts", "E.164 and extension collision check"),
|
|
274
|
+
("users", "Duplicate users", f"{store.count_by_type('user')} users to check against Webex"),
|
|
275
|
+
("rate-limit", "Rate limit budget", "API call estimate from plan_operations"),
|
|
276
|
+
("e911-readiness", "E911 readiness", f"{store.count_by_type('user')} users ECBN candidate check"),
|
|
277
|
+
("bulk-job-support", "Bulk device job support", "Probe for FedRAMP bulk device job support"),
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
results = []
|
|
281
|
+
for key, name, detail in checks_info:
|
|
282
|
+
if check_filter and check_filter != key:
|
|
283
|
+
continue
|
|
284
|
+
results.append(CheckResult(
|
|
285
|
+
name=name,
|
|
286
|
+
status=CheckStatus.SKIP,
|
|
287
|
+
detail=f"[dry-run] Would check: {detail}",
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
return PreflightResult(
|
|
291
|
+
overall=CheckStatus.SKIP,
|
|
292
|
+
checks=results,
|
|
293
|
+
)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Per-endpoint rate limiter with exponential backoff.
|
|
2
|
+
|
|
3
|
+
Enforces max concurrent requests and handles 429 (Too Many Requests) responses
|
|
4
|
+
with exponential backoff. Configurable via migration settings.
|
|
5
|
+
|
|
6
|
+
(from cucm-wxc-migration.md lines 443-444,
|
|
7
|
+
05-dependency-graph.md rate limit budget section)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class RateLimitConfig:
|
|
19
|
+
"""Configuration for the rate limiter."""
|
|
20
|
+
max_concurrent: int = 5
|
|
21
|
+
base_delay: float = 1.0 # seconds
|
|
22
|
+
max_delay: float = 60.0 # seconds
|
|
23
|
+
backoff_factor: float = 2.0
|
|
24
|
+
max_retries: int = 5
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class _EndpointState:
|
|
29
|
+
"""Tracks backoff state for a single endpoint."""
|
|
30
|
+
consecutive_429s: int = 0
|
|
31
|
+
next_allowed_at: float = 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RateLimiter:
|
|
35
|
+
"""Per-endpoint rate limiter with exponential backoff on 429 responses.
|
|
36
|
+
|
|
37
|
+
Usage (sync):
|
|
38
|
+
limiter = RateLimiter()
|
|
39
|
+
with limiter.acquire("/v1/people"):
|
|
40
|
+
response = make_api_call()
|
|
41
|
+
if response.status_code == 429:
|
|
42
|
+
limiter.record_429("/v1/people")
|
|
43
|
+
|
|
44
|
+
Usage (async):
|
|
45
|
+
limiter = RateLimiter()
|
|
46
|
+
async with limiter.acquire_async("/v1/people"):
|
|
47
|
+
response = await make_api_call()
|
|
48
|
+
if response.status_code == 429:
|
|
49
|
+
limiter.record_429("/v1/people")
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: RateLimitConfig | None = None) -> None:
|
|
53
|
+
self.config = config or RateLimitConfig()
|
|
54
|
+
self._semaphore = asyncio.Semaphore(self.config.max_concurrent)
|
|
55
|
+
self._sync_count: int = 0
|
|
56
|
+
self._sync_count_lock = None # lazy init for threading
|
|
57
|
+
self._endpoints: dict[str, _EndpointState] = {}
|
|
58
|
+
|
|
59
|
+
def _get_endpoint(self, endpoint: str) -> _EndpointState:
|
|
60
|
+
if endpoint not in self._endpoints:
|
|
61
|
+
self._endpoints[endpoint] = _EndpointState()
|
|
62
|
+
return self._endpoints[endpoint]
|
|
63
|
+
|
|
64
|
+
def record_429(self, endpoint: str) -> float:
|
|
65
|
+
"""Record a 429 response. Returns the delay (seconds) before next retry."""
|
|
66
|
+
state = self._get_endpoint(endpoint)
|
|
67
|
+
state.consecutive_429s += 1
|
|
68
|
+
delay = min(
|
|
69
|
+
self.config.base_delay * (self.config.backoff_factor ** state.consecutive_429s),
|
|
70
|
+
self.config.max_delay,
|
|
71
|
+
)
|
|
72
|
+
state.next_allowed_at = time.monotonic() + delay
|
|
73
|
+
return delay
|
|
74
|
+
|
|
75
|
+
def record_success(self, endpoint: str) -> None:
|
|
76
|
+
"""Record a successful response, resetting backoff."""
|
|
77
|
+
state = self._get_endpoint(endpoint)
|
|
78
|
+
state.consecutive_429s = 0
|
|
79
|
+
state.next_allowed_at = 0.0
|
|
80
|
+
|
|
81
|
+
def should_retry(self, endpoint: str) -> bool:
|
|
82
|
+
"""Check if retries are still allowed for this endpoint."""
|
|
83
|
+
state = self._get_endpoint(endpoint)
|
|
84
|
+
return state.consecutive_429s < self.config.max_retries
|
|
85
|
+
|
|
86
|
+
def wait_time(self, endpoint: str) -> float:
|
|
87
|
+
"""Seconds to wait before the next request to this endpoint."""
|
|
88
|
+
state = self._get_endpoint(endpoint)
|
|
89
|
+
remaining = state.next_allowed_at - time.monotonic()
|
|
90
|
+
return max(0.0, remaining)
|
|
91
|
+
|
|
92
|
+
# -- Sync context manager --
|
|
93
|
+
|
|
94
|
+
class _SyncSlot:
|
|
95
|
+
"""Sync concurrency slot.
|
|
96
|
+
|
|
97
|
+
Note: The sync acquire path uses a simple counter and is NOT
|
|
98
|
+
thread-safe. For threaded usage, use ``acquire_async`` with an
|
|
99
|
+
asyncio event loop. The executor uses the async path for
|
|
100
|
+
concurrent API requests.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, limiter: RateLimiter, endpoint: str) -> None:
|
|
104
|
+
self._limiter = limiter
|
|
105
|
+
self._endpoint = endpoint
|
|
106
|
+
|
|
107
|
+
def __enter__(self) -> _SyncSlot:
|
|
108
|
+
# Wait for per-endpoint backoff
|
|
109
|
+
wait = self._limiter.wait_time(self._endpoint)
|
|
110
|
+
if wait > 0:
|
|
111
|
+
time.sleep(wait)
|
|
112
|
+
# Enforce concurrency (simple counter — not thread-safe)
|
|
113
|
+
while self._limiter._sync_count >= self._limiter.config.max_concurrent:
|
|
114
|
+
time.sleep(0.05)
|
|
115
|
+
self._limiter._sync_count += 1
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, *args: object) -> None:
|
|
119
|
+
self._limiter._sync_count -= 1
|
|
120
|
+
|
|
121
|
+
def acquire(self, endpoint: str) -> _SyncSlot:
|
|
122
|
+
"""Sync context manager that enforces rate limits."""
|
|
123
|
+
return self._SyncSlot(self, endpoint)
|
|
124
|
+
|
|
125
|
+
# -- Async context manager --
|
|
126
|
+
|
|
127
|
+
class _AsyncSlot:
|
|
128
|
+
def __init__(self, limiter: RateLimiter, endpoint: str) -> None:
|
|
129
|
+
self._limiter = limiter
|
|
130
|
+
self._endpoint = endpoint
|
|
131
|
+
|
|
132
|
+
async def __aenter__(self) -> _AsyncSlot:
|
|
133
|
+
wait = self._limiter.wait_time(self._endpoint)
|
|
134
|
+
if wait > 0:
|
|
135
|
+
await asyncio.sleep(wait)
|
|
136
|
+
await self._limiter._semaphore.acquire()
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
async def __aexit__(self, *args: object) -> None:
|
|
140
|
+
self._limiter._semaphore.release()
|
|
141
|
+
|
|
142
|
+
def acquire_async(self, endpoint: str) -> _AsyncSlot:
|
|
143
|
+
"""Async context manager that enforces rate limits."""
|
|
144
|
+
return self._AsyncSlot(self, endpoint)
|