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.
Files changed (413) hide show
  1. wxcli/__init__.py +4 -0
  2. wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
  3. wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
  4. wxcli/_playbook/.claude/rules/cleanup.md +29 -0
  5. wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
  6. wxcli/_playbook/.claude/rules/org-health.md +14 -0
  7. wxcli/_playbook/.claude/settings.json +8 -0
  8. wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
  9. wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
  10. wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
  11. wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
  12. wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
  13. wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
  14. wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
  15. wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
  16. wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
  17. wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
  18. wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
  19. wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
  20. wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
  21. wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
  22. wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
  23. wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
  24. wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
  25. wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
  26. wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
  27. wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
  28. wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
  29. wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
  30. wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
  31. wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
  32. wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
  33. wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
  34. wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
  35. wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
  36. wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
  37. wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
  38. wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
  39. wxcli/_playbook/CLAUDE.md +373 -0
  40. wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
  41. wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
  42. wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
  43. wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
  44. wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
  45. wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
  46. wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
  47. wxcli/_playbook/docs/reference/admin-partner.md +357 -0
  48. wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
  49. wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
  50. wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
  51. wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
  52. wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
  53. wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
  54. wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
  55. wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
  56. wxcli/_playbook/docs/reference/authentication.md +965 -0
  57. wxcli/_playbook/docs/reference/call-control.md +1205 -0
  58. wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
  59. wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
  60. wxcli/_playbook/docs/reference/call-routing.md +2304 -0
  61. wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
  62. wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
  63. wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
  64. wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
  65. wxcli/_playbook/docs/reference/devices-core.md +1869 -0
  66. wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
  67. wxcli/_playbook/docs/reference/devices-platform.md +467 -0
  68. wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
  69. wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
  70. wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
  71. wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
  72. wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
  73. wxcli/_playbook/docs/reference/meetings-content.md +473 -0
  74. wxcli/_playbook/docs/reference/meetings-core.md +903 -0
  75. wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
  76. wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
  77. wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
  78. wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
  79. wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
  80. wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
  81. wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
  82. wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
  83. wxcli/_playbook/docs/reference/provisioning.md +1366 -0
  84. wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
  85. wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
  86. wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
  87. wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
  88. wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
  89. wxcli/_version.py +24 -0
  90. wxcli/auth.py +137 -0
  91. wxcli/commands/__init__.py +0 -0
  92. wxcli/commands/_registry.py +181 -0
  93. wxcli/commands/activation_email.py +103 -0
  94. wxcli/commands/admin_recordings.py +585 -0
  95. wxcli/commands/analytics.py +105 -0
  96. wxcli/commands/announcement_playlists.py +219 -0
  97. wxcli/commands/announcements.py +519 -0
  98. wxcli/commands/archive_users.py +36 -0
  99. wxcli/commands/attachment_actions.py +60 -0
  100. wxcli/commands/audit_events.py +83 -0
  101. wxcli/commands/authorizations.py +113 -0
  102. wxcli/commands/auto_attendant.py +642 -0
  103. wxcli/commands/broadworks_billing_reports.py +126 -0
  104. wxcli/commands/broadworks_enterprises.py +160 -0
  105. wxcli/commands/broadworks_subscribers.py +319 -0
  106. wxcli/commands/broadworks_workspaces.py +108 -0
  107. wxcli/commands/call_controls.py +1304 -0
  108. wxcli/commands/call_park.py +494 -0
  109. wxcli/commands/call_pickup.py +229 -0
  110. wxcli/commands/call_queue.py +1878 -0
  111. wxcli/commands/call_recording.py +707 -0
  112. wxcli/commands/call_routing.py +1610 -0
  113. wxcli/commands/call_settings_for_me_phase_5.py +212 -0
  114. wxcli/commands/caller_reputation.py +169 -0
  115. wxcli/commands/calling_service.py +305 -0
  116. wxcli/commands/cc_address_book.py +770 -0
  117. wxcli/commands/cc_agent_greetings.py +242 -0
  118. wxcli/commands/cc_agent_summaries.py +39 -0
  119. wxcli/commands/cc_agent_wellbeing.py +216 -0
  120. wxcli/commands/cc_agents.py +425 -0
  121. wxcli/commands/cc_ai_assistant.py +55 -0
  122. wxcli/commands/cc_ai_feature.py +294 -0
  123. wxcli/commands/cc_audio_files.py +149 -0
  124. wxcli/commands/cc_auto_csat.py +311 -0
  125. wxcli/commands/cc_aux_code.py +458 -0
  126. wxcli/commands/cc_business_hour.py +353 -0
  127. wxcli/commands/cc_call_monitoring.py +228 -0
  128. wxcli/commands/cc_callbacks.py +194 -0
  129. wxcli/commands/cc_campaign.py +212 -0
  130. wxcli/commands/cc_captures.py +39 -0
  131. wxcli/commands/cc_contact_list.py +169 -0
  132. wxcli/commands/cc_contact_number.py +295 -0
  133. wxcli/commands/cc_data_sources.py +235 -0
  134. wxcli/commands/cc_desktop_layout.py +380 -0
  135. wxcli/commands/cc_desktop_profile.py +322 -0
  136. wxcli/commands/cc_dial_number.py +511 -0
  137. wxcli/commands/cc_dial_plan.py +347 -0
  138. wxcli/commands/cc_dnc.py +101 -0
  139. wxcli/commands/cc_entry_point.py +545 -0
  140. wxcli/commands/cc_ewt.py +51 -0
  141. wxcli/commands/cc_flow.py +477 -0
  142. wxcli/commands/cc_global_vars.py +430 -0
  143. wxcli/commands/cc_holiday_list.py +335 -0
  144. wxcli/commands/cc_journey.py +1261 -0
  145. wxcli/commands/cc_legacy_flows.py +49 -0
  146. wxcli/commands/cc_multimedia_profile.py +436 -0
  147. wxcli/commands/cc_notification.py +51 -0
  148. wxcli/commands/cc_outdial_ani.py +604 -0
  149. wxcli/commands/cc_overrides.py +341 -0
  150. wxcli/commands/cc_queue.py +1083 -0
  151. wxcli/commands/cc_queue_stats.py +55 -0
  152. wxcli/commands/cc_realtime.py +51 -0
  153. wxcli/commands/cc_resource_collection.py +312 -0
  154. wxcli/commands/cc_search.py +46 -0
  155. wxcli/commands/cc_site.py +400 -0
  156. wxcli/commands/cc_skill.py +350 -0
  157. wxcli/commands/cc_skill_profile.py +289 -0
  158. wxcli/commands/cc_subscriptions.py +426 -0
  159. wxcli/commands/cc_summaries.py +130 -0
  160. wxcli/commands/cc_tasks.py +844 -0
  161. wxcli/commands/cc_team.py +325 -0
  162. wxcli/commands/cc_user_profiles.py +349 -0
  163. wxcli/commands/cc_users.py +701 -0
  164. wxcli/commands/cc_work_types.py +363 -0
  165. wxcli/commands/cdr.py +89 -0
  166. wxcli/commands/classifications.py +38 -0
  167. wxcli/commands/cleanup.py +1427 -0
  168. wxcli/commands/client_settings.py +72 -0
  169. wxcli/commands/conference.py +328 -0
  170. wxcli/commands/configure.py +72 -0
  171. wxcli/commands/converged_recordings.py +347 -0
  172. wxcli/commands/converged_recordings_export.py +310 -0
  173. wxcli/commands/cq_playlists.py +42 -0
  174. wxcli/commands/cucm.py +2794 -0
  175. wxcli/commands/cucm_config.py +129 -0
  176. wxcli/commands/customer_assist.py +373 -0
  177. wxcli/commands/data_sources.py +227 -0
  178. wxcli/commands/dect_devices.py +787 -0
  179. wxcli/commands/device_configurations.py +77 -0
  180. wxcli/commands/device_dynamic_settings.py +320 -0
  181. wxcli/commands/device_settings.py +1489 -0
  182. wxcli/commands/devices.py +280 -0
  183. wxcli/commands/domains.py +142 -0
  184. wxcli/commands/ecm.py +178 -0
  185. wxcli/commands/emergency_services.py +827 -0
  186. wxcli/commands/events.py +83 -0
  187. wxcli/commands/external_voicemail.py +52 -0
  188. wxcli/commands/groups.py +206 -0
  189. wxcli/commands/guest_management.py +76 -0
  190. wxcli/commands/hds.py +340 -0
  191. wxcli/commands/hot_desk.py +67 -0
  192. wxcli/commands/hot_desking_members.py +118 -0
  193. wxcli/commands/hot_desking_portal.py +124 -0
  194. wxcli/commands/hunt_group.py +577 -0
  195. wxcli/commands/hybrid_clusters.py +71 -0
  196. wxcli/commands/hybrid_connectors.py +67 -0
  197. wxcli/commands/identity_org.py +159 -0
  198. wxcli/commands/init_playbook.py +155 -0
  199. wxcli/commands/licenses.py +106 -0
  200. wxcli/commands/live_monitoring.py +40 -0
  201. wxcli/commands/location_call_handling.py +577 -0
  202. wxcli/commands/location_schedules.py +341 -0
  203. wxcli/commands/location_settings.py +1297 -0
  204. wxcli/commands/location_voicemail.py +494 -0
  205. wxcli/commands/locations.py +336 -0
  206. wxcli/commands/meeting_captions.py +110 -0
  207. wxcli/commands/meeting_chats.py +63 -0
  208. wxcli/commands/meeting_invitees.py +234 -0
  209. wxcli/commands/meeting_messages.py +28 -0
  210. wxcli/commands/meeting_participants.py +278 -0
  211. wxcli/commands/meeting_polls.py +111 -0
  212. wxcli/commands/meeting_preferences.py +515 -0
  213. wxcli/commands/meeting_qa.py +78 -0
  214. wxcli/commands/meeting_qualities.py +43 -0
  215. wxcli/commands/meeting_reports.py +101 -0
  216. wxcli/commands/meeting_session_types.py +105 -0
  217. wxcli/commands/meeting_site.py +57 -0
  218. wxcli/commands/meeting_slido.py +44 -0
  219. wxcli/commands/meeting_summaries.py +92 -0
  220. wxcli/commands/meeting_tracking_codes.py +233 -0
  221. wxcli/commands/meeting_transcripts.py +230 -0
  222. wxcli/commands/meetings.py +1997 -0
  223. wxcli/commands/memberships.py +164 -0
  224. wxcli/commands/messages.py +213 -0
  225. wxcli/commands/mode_management.py +244 -0
  226. wxcli/commands/my_call_settings.py +3577 -0
  227. wxcli/commands/numbers.py +405 -0
  228. wxcli/commands/operating_modes.py +410 -0
  229. wxcli/commands/org_contacts.py +284 -0
  230. wxcli/commands/org_health_cli.py +55 -0
  231. wxcli/commands/org_settings.py +71 -0
  232. wxcli/commands/organizations.py +83 -0
  233. wxcli/commands/paging_group.py +257 -0
  234. wxcli/commands/partner_admins.py +141 -0
  235. wxcli/commands/partner_reports.py +188 -0
  236. wxcli/commands/partner_tags.py +216 -0
  237. wxcli/commands/people.py +279 -0
  238. wxcli/commands/person_call_settings.py +71 -0
  239. wxcli/commands/pstn.py +280 -0
  240. wxcli/commands/recording_report.py +154 -0
  241. wxcli/commands/report_templates.py +38 -0
  242. wxcli/commands/reports.py +144 -0
  243. wxcli/commands/resource_group_memberships.py +160 -0
  244. wxcli/commands/resource_groups.py +67 -0
  245. wxcli/commands/roles.py +63 -0
  246. wxcli/commands/room_tabs.py +160 -0
  247. wxcli/commands/rooms.py +225 -0
  248. wxcli/commands/scim_bulk.py +46 -0
  249. wxcli/commands/scim_groups.py +246 -0
  250. wxcli/commands/scim_schemas.py +82 -0
  251. wxcli/commands/scim_users.py +283 -0
  252. wxcli/commands/security_audit.py +54 -0
  253. wxcli/commands/service_apps.py +51 -0
  254. wxcli/commands/single_number_reach.py +209 -0
  255. wxcli/commands/team_memberships.py +155 -0
  256. wxcli/commands/teams.py +153 -0
  257. wxcli/commands/ucm_profile.py +42 -0
  258. wxcli/commands/update.py +219 -0
  259. wxcli/commands/user_settings.py +5116 -0
  260. wxcli/commands/video_mesh.py +1090 -0
  261. wxcli/commands/virtual_extensions.py +502 -0
  262. wxcli/commands/virtual_line_settings.py +2072 -0
  263. wxcli/commands/webhooks.py +176 -0
  264. wxcli/commands/wholesale_billing_reports.py +147 -0
  265. wxcli/commands/wholesale_provisioning.py +523 -0
  266. wxcli/commands/workspace_locations.py +339 -0
  267. wxcli/commands/workspace_metrics.py +103 -0
  268. wxcli/commands/workspace_personalization.py +70 -0
  269. wxcli/commands/workspace_settings.py +3347 -0
  270. wxcli/commands/workspaces.py +279 -0
  271. wxcli/commands/xapi.py +114 -0
  272. wxcli/config.py +139 -0
  273. wxcli/errors.py +77 -0
  274. wxcli/main.py +210 -0
  275. wxcli/migration/CLAUDE.md +106 -0
  276. wxcli/migration/__init__.py +1 -0
  277. wxcli/migration/advisory/CLAUDE.md +196 -0
  278. wxcli/migration/advisory/__init__.py +46 -0
  279. wxcli/migration/advisory/advisor.py +107 -0
  280. wxcli/migration/advisory/advisory_patterns.py +2552 -0
  281. wxcli/migration/advisory/recommendation_rules.py +835 -0
  282. wxcli/migration/cucm/CLAUDE.md +74 -0
  283. wxcli/migration/cucm/__init__.py +5 -0
  284. wxcli/migration/cucm/connection.py +265 -0
  285. wxcli/migration/cucm/discovery.py +225 -0
  286. wxcli/migration/cucm/extractors/__init__.py +1 -0
  287. wxcli/migration/cucm/extractors/announcements.py +92 -0
  288. wxcli/migration/cucm/extractors/base.py +90 -0
  289. wxcli/migration/cucm/extractors/device_profiles.py +98 -0
  290. wxcli/migration/cucm/extractors/devices.py +193 -0
  291. wxcli/migration/cucm/extractors/e911.py +103 -0
  292. wxcli/migration/cucm/extractors/features.py +309 -0
  293. wxcli/migration/cucm/extractors/helpers.py +73 -0
  294. wxcli/migration/cucm/extractors/informational.py +295 -0
  295. wxcli/migration/cucm/extractors/locations.py +221 -0
  296. wxcli/migration/cucm/extractors/moh.py +100 -0
  297. wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
  298. wxcli/migration/cucm/extractors/routing.py +493 -0
  299. wxcli/migration/cucm/extractors/shared_lines.py +146 -0
  300. wxcli/migration/cucm/extractors/templates.py +229 -0
  301. wxcli/migration/cucm/extractors/tier4.py +173 -0
  302. wxcli/migration/cucm/extractors/users.py +240 -0
  303. wxcli/migration/cucm/extractors/voicemail.py +243 -0
  304. wxcli/migration/cucm/extractors/workspaces.py +88 -0
  305. wxcli/migration/cucm/unity_connection.py +304 -0
  306. wxcli/migration/execute/CLAUDE.md +372 -0
  307. wxcli/migration/execute/__init__.py +303 -0
  308. wxcli/migration/execute/batch.py +277 -0
  309. wxcli/migration/execute/dependency.py +483 -0
  310. wxcli/migration/execute/engine.py +964 -0
  311. wxcli/migration/execute/handlers.py +2232 -0
  312. wxcli/migration/execute/planner.py +1939 -0
  313. wxcli/migration/execute/runtime.py +571 -0
  314. wxcli/migration/export/CLAUDE.md +49 -0
  315. wxcli/migration/export/__init__.py +8 -0
  316. wxcli/migration/export/csv_export.py +126 -0
  317. wxcli/migration/export/deployment_plan.py +479 -0
  318. wxcli/migration/export/json_export.py +60 -0
  319. wxcli/migration/models.py +941 -0
  320. wxcli/migration/phone_models.py +50 -0
  321. wxcli/migration/preflight/CLAUDE.md +52 -0
  322. wxcli/migration/preflight/__init__.py +89 -0
  323. wxcli/migration/preflight/checks.py +911 -0
  324. wxcli/migration/preflight/runner.py +293 -0
  325. wxcli/migration/rate_limiter.py +144 -0
  326. wxcli/migration/report/CLAUDE.md +245 -0
  327. wxcli/migration/report/__init__.py +7 -0
  328. wxcli/migration/report/appendix.py +2437 -0
  329. wxcli/migration/report/assembler.py +183 -0
  330. wxcli/migration/report/charts.py +263 -0
  331. wxcli/migration/report/executive.py +776 -0
  332. wxcli/migration/report/explainer.py +704 -0
  333. wxcli/migration/report/helpers.py +74 -0
  334. wxcli/migration/report/ingest.py +152 -0
  335. wxcli/migration/report/notice_templates.py +192 -0
  336. wxcli/migration/report/score.py +375 -0
  337. wxcli/migration/report/styles.py +1052 -0
  338. wxcli/migration/report/user_diff.py +1052 -0
  339. wxcli/migration/report/user_notice.py +425 -0
  340. wxcli/migration/state.py +139 -0
  341. wxcli/migration/store.py +860 -0
  342. wxcli/migration/transform/CLAUDE.md +107 -0
  343. wxcli/migration/transform/__init__.py +12 -0
  344. wxcli/migration/transform/analysis_pipeline.py +453 -0
  345. wxcli/migration/transform/analyzers/__init__.py +142 -0
  346. wxcli/migration/transform/analyzers/css_permission.py +189 -0
  347. wxcli/migration/transform/analyzers/css_routing.py +335 -0
  348. wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
  349. wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
  350. wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
  351. wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
  352. wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
  353. wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
  354. wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
  355. wxcli/migration/transform/analyzers/missing_data.py +300 -0
  356. wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
  357. wxcli/migration/transform/analyzers/shared_line.py +170 -0
  358. wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
  359. wxcli/migration/transform/analyzers/workspace_license.py +212 -0
  360. wxcli/migration/transform/cross_reference.py +1136 -0
  361. wxcli/migration/transform/cucm_pattern.py +246 -0
  362. wxcli/migration/transform/decisions.py +301 -0
  363. wxcli/migration/transform/e164.py +145 -0
  364. wxcli/migration/transform/engine.py +256 -0
  365. wxcli/migration/transform/mappers/CLAUDE.md +123 -0
  366. wxcli/migration/transform/mappers/__init__.py +76 -0
  367. wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
  368. wxcli/migration/transform/mappers/base.py +190 -0
  369. wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
  370. wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
  371. wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
  372. wxcli/migration/transform/mappers/css_mapper.py +868 -0
  373. wxcli/migration/transform/mappers/dect_mapper.py +367 -0
  374. wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
  375. wxcli/migration/transform/mappers/device_mapper.py +227 -0
  376. wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
  377. wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
  378. wxcli/migration/transform/mappers/e911_mapper.py +129 -0
  379. wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
  380. wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
  381. wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
  382. wxcli/migration/transform/mappers/line_mapper.py +310 -0
  383. wxcli/migration/transform/mappers/location_mapper.py +235 -0
  384. wxcli/migration/transform/mappers/moh_mapper.py +114 -0
  385. wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
  386. wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
  387. wxcli/migration/transform/mappers/routing_mapper.py +821 -0
  388. wxcli/migration/transform/mappers/snr_mapper.py +201 -0
  389. wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
  390. wxcli/migration/transform/mappers/user_mapper.py +264 -0
  391. wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
  392. wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
  393. wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
  394. wxcli/migration/transform/normalizers.py +2008 -0
  395. wxcli/migration/transform/pattern_converter.py +106 -0
  396. wxcli/migration/transform/pipeline.py +271 -0
  397. wxcli/migration/transform/rules.py +255 -0
  398. wxcli/org_health/CLAUDE.md +63 -0
  399. wxcli/org_health/__init__.py +7 -0
  400. wxcli/org_health/analyze.py +93 -0
  401. wxcli/org_health/checks.py +458 -0
  402. wxcli/org_health/collector.py +62 -0
  403. wxcli/org_health/models.py +78 -0
  404. wxcli/org_health/report.py +300 -0
  405. wxcli/output.py +101 -0
  406. wxcli-1.2.0.dist-info/METADATA +505 -0
  407. wxcli-1.2.0.dist-info/RECORD +413 -0
  408. wxcli-1.2.0.dist-info/WHEEL +5 -0
  409. wxcli-1.2.0.dist-info/entry_points.txt +2 -0
  410. wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
  411. wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
  412. wxcli-1.2.0.dist-info/scm_version.json +8 -0
  413. wxcli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,425 @@
1
+ """User communication notice generator.
2
+
3
+ Reads the post-analyze migration store, classifies each user into
4
+ communication scenarios, and assembles an HTML or plain-text notice
5
+ document. The SE customizes brand, date, and helpdesk contact via CLI flags.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html as html_mod
11
+ import re as re_mod
12
+ from collections import defaultdict
13
+ from dataclasses import dataclass, field
14
+
15
+ from wxcli.migration.store import MigrationStore
16
+
17
+ from wxcli.migration.report.notice_templates import (
18
+ AUDIENCE_FILTERS,
19
+ FOOTER_TEMPLATE,
20
+ INTRO_TEMPLATE,
21
+ SCENARIO_ORDER,
22
+ SCENARIOS,
23
+ TIMELINE_TEMPLATE,
24
+ )
25
+
26
+
27
+ @dataclass
28
+ class UserScenario:
29
+ """A user and their applicable communication scenarios."""
30
+
31
+ user_canonical_id: str
32
+ display_name: str
33
+ extension: str | None
34
+ scenarios: list[str] = field(default_factory=list)
35
+ device_model: str | None = None
36
+ device_tier: str | None = None
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Scenario detection
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ def _build_scenario_matrix(store: MigrationStore) -> list[UserScenario]:
45
+ """Scan store and classify each user into applicable scenarios."""
46
+ users = store.get_objects("user")
47
+ devices = store.get_objects("device")
48
+ decisions = store.get_all_decisions()
49
+
50
+ # Build lookup: owner_canonical_id -> device dict
51
+ device_by_owner: dict[str, dict] = {}
52
+ for d in devices:
53
+ owner = d.get("owner_canonical_id")
54
+ if owner:
55
+ device_by_owner[owner] = d
56
+
57
+ # Build lookup: user_canonical_id -> list of decisions
58
+ decisions_by_user: dict[str, list[dict]] = defaultdict(list)
59
+ for dec in decisions:
60
+ ctx = dec.get("context", {})
61
+ for key in ("user_canonical_id", "affected_user", "owner_canonical_id"):
62
+ uid = ctx.get(key)
63
+ if uid:
64
+ decisions_by_user[uid].append(dec)
65
+
66
+ # Build set of users with voicemail (via cross-ref or object field)
67
+ vm_cross_refs = store.get_cross_refs(relationship="user_has_voicemail_profile")
68
+ users_with_vm: set[str] = {ref["from_id"] for ref in vm_cross_refs}
69
+ for vm in store.get_objects("voicemail_profile"):
70
+ uid = vm.get("user_canonical_id")
71
+ if uid:
72
+ users_with_vm.add(uid)
73
+
74
+ # Build set of users with unmapped layout buttons
75
+ users_with_layout_issues: set[str] = set()
76
+ for layout in store.get_objects("device_layout"):
77
+ if layout.get("unmapped_buttons"):
78
+ owner = layout.get("owner_canonical_id")
79
+ if owner:
80
+ users_with_layout_issues.add(owner)
81
+
82
+ # Classify each user
83
+ result: list[UserScenario] = []
84
+ for user in users:
85
+ uid = user["canonical_id"]
86
+ scenarios: list[str] = []
87
+ device = device_by_owner.get(uid)
88
+
89
+ # Scenario 1: Phone upgrade (convertible)
90
+ if device and device.get("compatibility_tier") == "convertible":
91
+ scenarios.append("phone_upgrade")
92
+
93
+ # Scenario 2: Webex App transition
94
+ if device and device.get("compatibility_tier") == "webex_app":
95
+ scenarios.append("webex_app_transition")
96
+
97
+ # Scenario 7: Incompatible device
98
+ if device and device.get("compatibility_tier") == "incompatible":
99
+ scenarios.append("device_replacement")
100
+
101
+ # Scenario 3: Call forwarding simplified
102
+ user_decisions = decisions_by_user.get(uid, [])
103
+ if any(d["type"] == "FORWARDING_LOSSY" for d in user_decisions):
104
+ scenarios.append("forwarding_simplified")
105
+
106
+ # Scenario 4: Voicemail re-record
107
+ if uid in users_with_vm:
108
+ scenarios.append("voicemail_rerecord")
109
+
110
+ # Scenario 5: BLF/speed dial changes
111
+ has_button_decision = any(
112
+ d["type"] in ("BUTTON_UNMAPPABLE", "FEATURE_APPROXIMATION")
113
+ and (
114
+ "button" in d.get("summary", "").lower()
115
+ or "blf" in d.get("summary", "").lower()
116
+ )
117
+ for d in user_decisions
118
+ )
119
+ if has_button_decision or uid in users_with_layout_issues:
120
+ scenarios.append("layout_changes")
121
+
122
+ # Scenario 6: Executive/assistant
123
+ if any(
124
+ "executive" in d.get("summary", "").lower()
125
+ or "assistant" in d.get("summary", "").lower()
126
+ for d in user_decisions
127
+ ):
128
+ scenarios.append("exec_assistant")
129
+
130
+ # Build display_name from first_name + last_name if not set
131
+ display_name = user.get("display_name") or ""
132
+ if not display_name:
133
+ parts = [user.get("first_name", ""), user.get("last_name", "")]
134
+ display_name = " ".join(p for p in parts if p)
135
+
136
+ result.append(UserScenario(
137
+ user_canonical_id=uid,
138
+ display_name=display_name,
139
+ extension=user.get("extension"),
140
+ scenarios=scenarios,
141
+ device_model=device.get("model") if device else None,
142
+ device_tier=str(device.get("compatibility_tier", "")) if device else None,
143
+ ))
144
+
145
+ return result
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Audience filtering
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def _get_active_scenarios(matrix: list[UserScenario]) -> set[str]:
154
+ """Return the set of scenario IDs that have at least one affected user."""
155
+ active: set[str] = set()
156
+ for user in matrix:
157
+ active.update(user.scenarios)
158
+ return active
159
+
160
+
161
+ def _filter_by_audience(
162
+ matrix: list[UserScenario], audience: str
163
+ ) -> list[UserScenario]:
164
+ """Filter the matrix to a specific audience segment."""
165
+ if audience == "all":
166
+ return matrix
167
+ if audience == "general":
168
+ device_scenarios = {"phone_upgrade", "webex_app_transition", "device_replacement"}
169
+ return [u for u in matrix if not (set(u.scenarios) & device_scenarios)]
170
+ allowed = AUDIENCE_FILTERS.get(audience, set())
171
+ if not allowed:
172
+ return matrix
173
+ return [u for u in matrix if set(u.scenarios) & allowed]
174
+
175
+
176
+ # ---------------------------------------------------------------------------
177
+ # HTML rendering
178
+ # ---------------------------------------------------------------------------
179
+
180
+
181
+ def _render_intro(brand: str, migration_date: str) -> str:
182
+ text = INTRO_TEMPLATE.format(
183
+ brand=html_mod.escape(brand),
184
+ migration_date=html_mod.escape(migration_date),
185
+ )
186
+ return f'<div class="notice-intro">\n{text}\n</div>'
187
+
188
+
189
+ def _render_scenario(
190
+ scenario_id: str,
191
+ affected_count: int,
192
+ brand: str,
193
+ helpdesk: str,
194
+ ) -> str:
195
+ scenario = SCENARIOS[scenario_id]
196
+ title = html_mod.escape(scenario["title"])
197
+ variables: dict[str, str] = {
198
+ "brand": html_mod.escape(brand),
199
+ "helpdesk": html_mod.escape(helpdesk),
200
+ "cucm_model": "",
201
+ "extension": "",
202
+ }
203
+ body = scenario["template"].format_map(variables)
204
+ count_label = f"Applies to {affected_count} user{'s' if affected_count != 1 else ''}"
205
+ return (
206
+ f'<div class="scenario-section">\n'
207
+ f"<h3>{title}</h3>\n"
208
+ f'<p class="affected-count">{count_label}</p>\n'
209
+ f"<p>{body}</p>\n"
210
+ f"</div>"
211
+ )
212
+
213
+
214
+ def _render_timeline(migration_date: str, helpdesk: str) -> str:
215
+ text = TIMELINE_TEMPLATE.format(
216
+ migration_date=html_mod.escape(migration_date),
217
+ helpdesk=html_mod.escape(helpdesk),
218
+ )
219
+ return f'<div class="notice-timeline">\n<h2>Migration Day Timeline</h2>\n{text}\n</div>'
220
+
221
+
222
+ def _render_footer(helpdesk: str, prepared_by: str, brand: str) -> str:
223
+ text = FOOTER_TEMPLATE.format(
224
+ helpdesk=html_mod.escape(helpdesk),
225
+ prepared_by=html_mod.escape(prepared_by) if prepared_by else "",
226
+ brand=html_mod.escape(brand),
227
+ )
228
+ return f'<div class="notice-footer">\n{text}\n</div>'
229
+
230
+
231
+ NOTICE_CSS: str = """
232
+ body {
233
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
234
+ line-height: 1.6;
235
+ color: #1a1f25;
236
+ background: #ffffff;
237
+ margin: 0;
238
+ padding: 0;
239
+ }
240
+ .notice-container {
241
+ max-width: 640px;
242
+ margin: 0 auto;
243
+ padding: 32px 24px;
244
+ }
245
+ .notice-intro {
246
+ margin-bottom: 24px;
247
+ }
248
+ .notice-intro strong:first-child {
249
+ font-size: 1.4em;
250
+ display: block;
251
+ margin-bottom: 12px;
252
+ color: #242a33;
253
+ }
254
+ .scenario-section {
255
+ border-left: 4px solid #00897B;
256
+ background: #f8faf9;
257
+ padding: 16px 20px;
258
+ margin: 16px 0;
259
+ border-radius: 0 4px 4px 0;
260
+ }
261
+ .scenario-section h3 {
262
+ margin-top: 0;
263
+ color: #242a33;
264
+ }
265
+ .affected-count {
266
+ font-size: 0.85em;
267
+ color: #636e7e;
268
+ margin-top: -8px;
269
+ margin-bottom: 8px;
270
+ }
271
+ .notice-timeline {
272
+ margin-top: 32px;
273
+ padding-top: 24px;
274
+ border-top: 1px solid #e0e0e0;
275
+ }
276
+ .notice-timeline h2 {
277
+ color: #242a33;
278
+ margin-top: 0;
279
+ }
280
+ .notice-footer {
281
+ margin-top: 32px;
282
+ padding-top: 24px;
283
+ border-top: 1px solid #e0e0e0;
284
+ font-size: 0.95em;
285
+ }
286
+ h2 { font-size: 1.25em; }
287
+ h3 { font-size: 1.1em; }
288
+ ul, ol { padding-left: 24px; }
289
+ a { color: #00897B; }
290
+ """
291
+
292
+
293
+ # ---------------------------------------------------------------------------
294
+ # Plain text rendering
295
+ # ---------------------------------------------------------------------------
296
+
297
+
298
+ def _strip_html(html_text: str) -> str:
299
+ """Convert HTML fragment to plain text."""
300
+ text = html_text
301
+ text = re_mod.sub(r"<strong>(.*?)</strong>", r"**\1**", text)
302
+ text = re_mod.sub(r"<em>(.*?)</em>", r"_\1_", text)
303
+ text = re_mod.sub(r"<li>(.*?)</li>", r" - \1", text)
304
+ text = re_mod.sub(r'<a href="([^"]*)">(.*?)</a>', r"\2 (\1)", text)
305
+ text = re_mod.sub(r"<[^>]+>", "", text)
306
+ text = re_mod.sub(r"\n{3,}", "\n\n", text)
307
+ return text.strip()
308
+
309
+
310
+ def _render_plain_intro(brand: str, migration_date: str) -> str:
311
+ text = INTRO_TEMPLATE.format(brand=brand, migration_date=migration_date)
312
+ return _strip_html(text)
313
+
314
+
315
+ def _render_plain_scenario(
316
+ scenario_id: str,
317
+ affected_count: int,
318
+ brand: str,
319
+ helpdesk: str,
320
+ ) -> str:
321
+ scenario = SCENARIOS[scenario_id]
322
+ title = scenario["title"]
323
+ variables: dict[str, str] = {
324
+ "brand": brand,
325
+ "helpdesk": helpdesk,
326
+ "cucm_model": "",
327
+ "extension": "",
328
+ }
329
+ body = _strip_html(scenario["template"].format_map(variables))
330
+ count_label = f"Applies to {affected_count} user{'s' if affected_count != 1 else ''}"
331
+ underline = "-" * len(title)
332
+ return f"{title}\n{underline}\n{count_label}\n\n{body}"
333
+
334
+
335
+ def _render_plain_timeline(migration_date: str, helpdesk: str) -> str:
336
+ text = _strip_html(
337
+ TIMELINE_TEMPLATE.format(migration_date=migration_date, helpdesk=helpdesk)
338
+ )
339
+ title = "Migration Day Timeline"
340
+ return f"{title}\n{'=' * len(title)}\n\n{text}"
341
+
342
+
343
+ def _render_plain_footer(helpdesk: str, prepared_by: str, brand: str) -> str:
344
+ return _strip_html(
345
+ FOOTER_TEMPLATE.format(
346
+ helpdesk=helpdesk, prepared_by=prepared_by or "", brand=brand
347
+ )
348
+ )
349
+
350
+
351
+ def _assemble_plain_text(
352
+ matrix: list[UserScenario],
353
+ active_scenarios: set[str],
354
+ brand: str,
355
+ migration_date: str,
356
+ helpdesk: str,
357
+ prepared_by: str,
358
+ ) -> str:
359
+ sections: list[str] = [_render_plain_intro(brand, migration_date)]
360
+ for scenario_id in SCENARIO_ORDER:
361
+ if scenario_id in active_scenarios:
362
+ affected_count = sum(1 for u in matrix if scenario_id in u.scenarios)
363
+ sections.append(
364
+ _render_plain_scenario(scenario_id, affected_count, brand, helpdesk)
365
+ )
366
+ sections.append(_render_plain_timeline(migration_date, helpdesk))
367
+ sections.append(_render_plain_footer(helpdesk, prepared_by, brand))
368
+ return "\n\n\n".join(sections)
369
+
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # Public API
373
+ # ---------------------------------------------------------------------------
374
+
375
+
376
+ def generate_user_notice(
377
+ store: MigrationStore,
378
+ brand: str,
379
+ migration_date: str,
380
+ helpdesk: str,
381
+ prepared_by: str = "",
382
+ audience: str = "all",
383
+ text_only: bool = False,
384
+ ) -> str:
385
+ """Generate user-facing migration communication document."""
386
+ matrix = _build_scenario_matrix(store)
387
+
388
+ if audience != "all":
389
+ matrix = _filter_by_audience(matrix, audience)
390
+
391
+ active_scenarios = _get_active_scenarios(matrix)
392
+
393
+ if text_only:
394
+ return _assemble_plain_text(
395
+ matrix, active_scenarios, brand, migration_date, helpdesk, prepared_by
396
+ )
397
+
398
+ # Assemble HTML
399
+ sections: list[str] = [_render_intro(brand, migration_date)]
400
+ for scenario_id in SCENARIO_ORDER:
401
+ if scenario_id in active_scenarios:
402
+ affected_count = sum(1 for u in matrix if scenario_id in u.scenarios)
403
+ sections.append(
404
+ _render_scenario(scenario_id, affected_count, brand, helpdesk)
405
+ )
406
+ sections.append(_render_timeline(migration_date, helpdesk))
407
+ sections.append(_render_footer(helpdesk, prepared_by, brand))
408
+
409
+ body = "\n\n".join(sections)
410
+
411
+ return (
412
+ "<!DOCTYPE html>\n"
413
+ '<html lang="en">\n'
414
+ "<head>\n"
415
+ '<meta charset="utf-8">\n'
416
+ f"<title>Migration Notice \u2014 {html_mod.escape(brand)}</title>\n"
417
+ f"<style>\n{NOTICE_CSS}</style>\n"
418
+ "</head>\n"
419
+ "<body>\n"
420
+ '<div class="notice-container">\n'
421
+ f"{body}\n"
422
+ "</div>\n"
423
+ "</body>\n"
424
+ "</html>"
425
+ )
@@ -0,0 +1,139 @@
1
+ """Project-level state machine for CUCM-to-Webex migration.
2
+
3
+ Tracks the current phase of a migration project. Persisted to state.json
4
+ (not SQLite — it's ~10 lines and read on every CLI invocation).
5
+
6
+ State diagram from: cucm-wxc-migration.md lines 228-261
7
+ Persistence pattern from: 01-data-representation.md lines 147-154
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from datetime import datetime, timezone
14
+ from enum import Enum
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ class ProjectState(str, Enum):
20
+ """Project-level migration states.
21
+ (from cucm-wxc-migration.md lines 230-254)
22
+ """
23
+ INITIALIZED = "initialized"
24
+ CONNECTED = "connected"
25
+ DISCOVERED = "discovered"
26
+ ANALYZED = "analyzed"
27
+ READY = "ready"
28
+ BLOCKED = "blocked"
29
+ PLANNED = "planned"
30
+ PREFLIGHT = "preflight"
31
+ PREFLIGHT_FAILED = "preflight_failed"
32
+ SNAPSHOTTED = "snapshotted"
33
+ EXECUTING = "executing"
34
+ FAILED = "failed"
35
+ ROLLED_BACK = "rolled_back"
36
+ VALIDATING = "validating"
37
+ COMPLETED = "completed"
38
+
39
+
40
+ # Valid transitions from each state.
41
+ # (from cucm-wxc-migration.md state diagram lines 228-261)
42
+ VALID_TRANSITIONS: dict[ProjectState, set[ProjectState]] = {
43
+ ProjectState.INITIALIZED: {ProjectState.CONNECTED},
44
+ ProjectState.CONNECTED: {ProjectState.DISCOVERED},
45
+ ProjectState.DISCOVERED: {ProjectState.ANALYZED},
46
+ ProjectState.ANALYZED: {ProjectState.READY, ProjectState.BLOCKED},
47
+ ProjectState.READY: {ProjectState.PLANNED},
48
+ ProjectState.BLOCKED: {ProjectState.PLANNED},
49
+ ProjectState.PLANNED: {ProjectState.PREFLIGHT},
50
+ ProjectState.PREFLIGHT: {ProjectState.PREFLIGHT_FAILED, ProjectState.SNAPSHOTTED, ProjectState.PREFLIGHT},
51
+ ProjectState.PREFLIGHT_FAILED: {ProjectState.SNAPSHOTTED, ProjectState.PREFLIGHT},
52
+ ProjectState.SNAPSHOTTED: {ProjectState.EXECUTING},
53
+ ProjectState.EXECUTING: {ProjectState.FAILED, ProjectState.VALIDATING},
54
+ ProjectState.FAILED: {ProjectState.ROLLED_BACK},
55
+ ProjectState.VALIDATING: {ProjectState.COMPLETED},
56
+ ProjectState.ROLLED_BACK: set(),
57
+ ProjectState.COMPLETED: set(),
58
+ }
59
+
60
+
61
+ class InvalidTransitionError(Exception):
62
+ """Raised when a state transition is not allowed."""
63
+
64
+
65
+ class MigrationState:
66
+ """State machine for a migration project with JSON persistence.
67
+
68
+ (from cucm-wxc-migration.md lines 228-261,
69
+ 01-data-representation.md lines 147-154)
70
+ """
71
+
72
+ def __init__(self, state_path: Path | str, project_id: str | None = None) -> None:
73
+ self.state_path = Path(state_path)
74
+ self._data: dict[str, Any] = self._load()
75
+ if project_id is not None and not self._data["project_id"]:
76
+ self._data["project_id"] = project_id
77
+ self._save()
78
+
79
+ @property
80
+ def state(self) -> ProjectState:
81
+ return ProjectState(self._data["state"])
82
+
83
+ @property
84
+ def project_id(self) -> str:
85
+ return self._data["project_id"]
86
+
87
+ @property
88
+ def history(self) -> list[dict[str, str]]:
89
+ return self._data.get("history", [])
90
+
91
+ def transition(self, target: ProjectState) -> None:
92
+ """Transition to a new state. Raises InvalidTransitionError if not allowed."""
93
+ current = self.state
94
+ if target not in VALID_TRANSITIONS.get(current, set()):
95
+ raise InvalidTransitionError(
96
+ f"Cannot transition from {current.value} to {target.value}. "
97
+ f"Valid targets: {sorted(s.value for s in VALID_TRANSITIONS.get(current, set()))}"
98
+ )
99
+ now = datetime.now(timezone.utc).isoformat()
100
+ self._data["history"].append({
101
+ "from": current.value,
102
+ "to": target.value,
103
+ "timestamp": now,
104
+ })
105
+ self._data["state"] = target.value
106
+ self._data["updated_at"] = now
107
+ self._save()
108
+
109
+ def _load(self) -> dict[str, Any]:
110
+ if self.state_path.exists():
111
+ with open(self.state_path, encoding="utf-8") as f:
112
+ return json.load(f)
113
+ return self._default_data()
114
+
115
+ def _save(self) -> None:
116
+ self.state_path.parent.mkdir(parents=True, exist_ok=True)
117
+ with open(self.state_path, "w", encoding="utf-8") as f:
118
+ json.dump(self._data, f, indent=2)
119
+
120
+ @staticmethod
121
+ def _default_data() -> dict[str, Any]:
122
+ now = datetime.now(timezone.utc).isoformat()
123
+ return {
124
+ "project_id": "",
125
+ "state": ProjectState.INITIALIZED.value,
126
+ "created_at": now,
127
+ "updated_at": now,
128
+ "history": [],
129
+ }
130
+
131
+ @classmethod
132
+ def create(cls, state_path: Path | str, project_id: str) -> MigrationState:
133
+ """Create a new migration state file."""
134
+ instance = cls.__new__(cls)
135
+ instance.state_path = Path(state_path)
136
+ instance._data = cls._default_data()
137
+ instance._data["project_id"] = project_id
138
+ instance._save()
139
+ return instance