From a3cfc0663082e863633f60fe3fe28e7e9b59e810 Mon Sep 17 00:00:00 2001 From: JOhn Date: Sat, 29 Aug 2026 12:30:34 -0400 Subject: [PATCH] update: fixes enum ticket handling --- gui/v2/actions/operation_result_dispatch.py | 74 +++++++++++++++++++++ gui/v2/actions/operation_results.py | 12 ++++ gui/v2/ui/pages/menu_page.py | 56 +++------------- gui/v2/workers/worker.py | 23 ++++++- 4 files changed, 116 insertions(+), 49 deletions(-) create mode 100755 gui/v2/actions/operation_result_dispatch.py diff --git a/gui/v2/actions/operation_result_dispatch.py b/gui/v2/actions/operation_result_dispatch.py new file mode 100755 index 0000000..52cb518 --- /dev/null +++ b/gui/v2/actions/operation_result_dispatch.py @@ -0,0 +1,74 @@ +import inspect +from enum import Enum +from typing import Any + +from core.controllers.ConfigurationController import ConfigurationController +from core.models.Configuration import ConnectionChoice +from core.models.Result import ResultError +from core.services.prepare_tickets import ticket_tracker + + +DEFAULT_ENUM_MAP = { + ResultError.SUBSCRIPTION: ( + ticket_tracker.wipe_one_ticket_sub, + {}, + ), + ResultError.CONNECTION: ( + ConfigurationController.set_connection_enum, + {'connection_enum': ConnectionChoice.SYSTEM}, + ), + ResultError.INVALID_API_REPLY: None, +} + + +def has_function(enum_map: dict[Enum, Any], enum_key: Enum) -> bool: + action = enum_map.get(enum_key) + if action is None: + return False + function, _ = _normalize_action(action) + return callable(function) + + +def error_dispatch(enum_map: dict[Enum, Any], enum_key: Enum, **kwargs: Any) -> Any: + if enum_key not in enum_map: + return None + action = enum_map[enum_key] + if action is None: + return None + function, default_kwargs = _normalize_action(action) + call_kwargs = dict(default_kwargs) + call_kwargs.update(kwargs) + return function(**_filter_kwargs(function, call_kwargs)) + + +def has_result_action(result, enum_map: dict[Enum, Any] | None = None) -> bool: + return has_function(enum_map or DEFAULT_ENUM_MAP, result.error_type) + + +def dispatch_result_action(result, enum_map: dict[Enum, Any] | None = None, **kwargs: Any) -> Any: + return error_dispatch(enum_map or DEFAULT_ENUM_MAP, result.error_type, **kwargs) + + +def _normalize_action(action: Any) -> tuple[Any, dict[str, Any]]: + if isinstance(action, tuple): + function = action[0] + if len(action) > 1 and isinstance(action[1], dict): + return function, action[1] + return function, {} + return action, {} + + +def _filter_kwargs(function: Any, kwargs: dict[str, Any]) -> dict[str, Any]: + signature = inspect.signature(function) + parameters = signature.parameters + if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters.values()): + return kwargs + accepted = { + name + for name, param in parameters.items() + if param.kind in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ) + } + return {key: value for key, value in kwargs.items() if key in accepted} diff --git a/gui/v2/actions/operation_results.py b/gui/v2/actions/operation_results.py index e652cf4..a9a976b 100755 --- a/gui/v2/actions/operation_results.py +++ b/gui/v2/actions/operation_results.py @@ -4,3 +4,15 @@ from core.models.Result import Result, ResultError def result_from_exception(exception: Exception, error_type: ResultError = ResultError.UNKNOWN) -> Result: message = str(exception) or type(exception).__name__ return Result(valid=False, error_type=error_type, message=message) + + +def result_from_payload(payload) -> Result: + if isinstance(payload, Result): + return payload + if isinstance(payload, Exception): + return result_from_exception(payload) + return Result( + valid=False, + error_type=ResultError.UNKNOWN, + message=str(payload) or 'The operation could not be completed.', + ) diff --git a/gui/v2/ui/pages/menu_page.py b/gui/v2/ui/pages/menu_page.py index d37e414..334bcf2 100755 --- a/gui/v2/ui/pages/menu_page.py +++ b/gui/v2/ui/pages/menu_page.py @@ -17,13 +17,13 @@ from core.controllers.tickets.UseTicketController import ( do_we_use_a_random_ticket, get_unused_tickets, ) -from core.models.Result import Result, ResultError from core.models.session.SessionProfile import SessionProfile from core.models.system.SystemProfile import SystemProfile from gui.v2.infrastructure.setup_observers import ticket_observer from gui.v2.actions.database_health import GuiStorageDatabaseError -from gui.v2.actions.operation_results import result_from_exception +from gui.v2.actions.operation_result_dispatch import has_result_action +from gui.v2.actions.operation_results import result_from_payload from gui.v2.actions.profile_order import normalize_profile_order from gui.v2.actions.profile_status import is_profile_enabled_for_gui from gui.v2.ui.pages.Page import Page @@ -79,8 +79,6 @@ class MenuPage(Page): self.button_states = {} self.is_system_connected = False self.profile_button_map = {} - self._pending_operation_name = None - self._pending_operation_profile_data = None self.font_style = f"font-family: '{main_window.open_sans_family}';" if main_window.open_sans_family else "" self.create_interface_elements() @@ -1259,8 +1257,6 @@ class MenuPage(Page): self.enabling_profile(profile_data) def enabling_profile(self, profile_data): - self._pending_operation_name = 'enable_profile' - self._pending_operation_profile_data = dict(profile_data) self.worker = Worker(profile_data) self.worker.update_signal.connect(self.update_gui_main_thread) self.worker.change_page.connect(self.change_app_page) @@ -1271,7 +1267,7 @@ class MenuPage(Page): thread.start() def handle_operation_failure(self, payload): - result = self._result_from_payload(payload) + result = result_from_payload(payload) if result.valid: if result.message: self.update_status.update_status(result.message) @@ -1284,7 +1280,7 @@ class MenuPage(Page): self.disconnect_button.setEnabled(True) self.disconnect_system_wide_button.setEnabled(True) - is_actionable = result.error_type in self._operation_choices() + is_actionable = has_result_action(result) self.popup = OperationResultPopup( self, @@ -1294,49 +1290,13 @@ class MenuPage(Page): action_result=is_actionable, ) self.popup.action_selected.connect( - lambda accepted, current_result=result: self.handle_operation_popup_action(accepted, current_result)) + lambda accepted: self.handle_operation_popup_action(accepted)) self.popup.show() - def _result_from_payload(self, payload): - if isinstance(payload, Result): - return payload - if isinstance(payload, Exception): - return result_from_exception(payload) - return Result( - valid=False, - error_type=ResultError.UNKNOWN, - message=str(payload) or 'The operation could not be completed.', - ) - - def handle_operation_popup_action(self, accepted, result): - if not accepted: + def handle_operation_popup_action(self, accepted): + if self.worker is None: return - self._action_for_result(result)(result) - - def _action_for_result(self, result): - return self._operation_choices().get(result.error_type, self._dismiss_operation_result) - - def _operation_choices(self): - CHOICES = { - ResultError.CONNECTION: self._retry_pending_operation, - ResultError.SUBSCRIPTION: self._handle_expired_ticket_result, - } - return CHOICES - - def _retry_pending_operation(self, result): - profile_data = self._pending_operation_profile_data - if not isinstance(profile_data, dict) or 'id' not in profile_data: - self.update_status.update_status('Retry is unavailable for this operation.') - return - self.update_status.update_status('Retrying...') - self.enabling_profile(dict(profile_data)) - - def _handle_expired_ticket_result(self, result): - self.update_status.update_status(result.message or 'Ticket expired.') - self._route_to_billing_entry() - - def _dismiss_operation_result(self, result): - self.update_status.update_status(result.message or 'The operation could not be completed.') + self.worker.handle_operation_popup_choice(accepted) def show_ticket_data_loss_popup(self, ticket_number, billing_code): self.custom_window.navigator.navigate("menu") diff --git a/gui/v2/workers/worker.py b/gui/v2/workers/worker.py index d1a5298..81a0cfe 100755 --- a/gui/v2/workers/worker.py +++ b/gui/v2/workers/worker.py @@ -25,6 +25,7 @@ from core.Errors import ( ) from gui.v2.actions.database_health import GuiStorageDatabaseError +from gui.v2.actions.operation_result_dispatch import dispatch_result_action from gui.v2.actions.operation_results import result_from_exception from gui.v2.infrastructure.screen_size import get_max_screensize from gui.v2.infrastructure.setup_observers import ( @@ -51,6 +52,8 @@ class Worker(QObject): self.profile_type = None self._ticket_error_emitted = False self._consumed_ticket = None + self._pending_operation_result = None + self._pending_operation_kwargs = {} def run(self): try: @@ -114,7 +117,7 @@ class Worker(QObject): application_version_observer=application_version_observer, connection_observer=connection_observer, ticket_observer=ticket_observer, max_resolution=max_resolution) if isinstance(enable_result, Result) and not enable_result.valid: - self.operation_failed.emit(enable_result) + self._emit_operation_failure(enable_result) return except EndpointVerificationError: self.update_signal.emit( @@ -247,8 +250,26 @@ class Worker(QObject): error_type=ResultError.UNKNOWN, message=message or 'Ticket use failed.', ) + self._emit_operation_failure(result, which_ticket=which_ticket) + + def _emit_operation_failure(self, result, **kwargs): + self._pending_operation_result = result + self._pending_operation_kwargs = { + key: value for key, value in kwargs.items() + if value is not None + } self.operation_failed.emit(result) + def handle_operation_popup_choice(self, accepted): + if not accepted: + return None + if not isinstance(self._pending_operation_result, Result): + return None + return dispatch_result_action( + self._pending_operation_result, + **self._pending_operation_kwargs, + ) + def _emit_subscription_lookup_failure(self, exception=None): message = ( "Ticket was accepted, but the subscription details could not be "