From eba5827189426b0ece570edbcfc96f9f9e4172b1 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 26 Jul 2026 15:27:12 +0545 Subject: [PATCH] feat: add sysauth service for Polkit authentication - Add SysauthService with D-Bus agent registration - Add AuthDialog with centered overlay password prompt - Add password utils (PAM verification, keyrings.alt caching) - Integrate sysauth into main app initialization - Update WiFi dialog to use sysauth for password prompts - Add sysauth.css with dialog styling - Remove standalone WiFi password dialog --- pyproject.toml | 2 + src/main.py | 5 + src/services/__init__.py | 1 + src/services/sysauth.py | 350 +++++++++++++++++++++ src/shared/dialogs/sysauth_dialog.py | 277 ++++++++++++++++ src/shared/dialogs/wifi_password_dialog.py | 269 ---------------- src/styles/main.css | 1 + src/styles/widgets/sysauth.css | 98 ++++++ src/utils/password.py | 137 ++++++++ src/window/controlcenter/wifi.py | 49 ++- uv.lock | 208 ++++++++++++ 11 files changed, 1100 insertions(+), 297 deletions(-) create mode 100644 src/services/sysauth.py create mode 100644 src/shared/dialogs/sysauth_dialog.py delete mode 100644 src/shared/dialogs/wifi_password_dialog.py create mode 100644 src/styles/widgets/sysauth.css create mode 100644 src/utils/password.py diff --git a/pyproject.toml b/pyproject.toml index 493183a..ab12a75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ classifiers = [ dependencies = [ "fabric", "httpx>=0.28.1", + "keyring>=23.0.0", + "keyrings-alt>=5.0.2", "psutil>=7.2.2", "python-pam>=2.0.2", "tomlkit>=0.15.0", diff --git a/src/main.py b/src/main.py index 0a48a3d..c9544b1 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,7 @@ from fabric.utils import get_relative_path, logger, monitor_file from services.config import config from services.keyboard_layout import KeyboardLayout +from services.sysauth import SysauthService from shared.data import APP_NAME, load_config from utils.functions import set_process_name from utils.gtk_utils import generate_colors_from_wallpaper @@ -96,6 +97,10 @@ def main(): # Set up styling before creating windows to prevent unstyled flashes setup_css(app) + # Initialize Polkit authentication agent + sysauth = SysauthService() + sysauth.start() + switcher = ApplicationSwitcher() panel = Panel() modusnoti = ModusNoti() diff --git a/src/services/__init__.py b/src/services/__init__.py index e105713..13cbb30 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -20,5 +20,6 @@ from .network import ( # noqa: F401 Wifi, ) from .numlock import NumLock # noqa: F401 +from .sysauth import SysauthService, get_sysauth_service # noqa: F401 from .todo import TodoService # noqa: F401 from .wallpaper import WallpaperService, create_thumbnail # noqa: F401 diff --git a/src/services/sysauth.py b/src/services/sysauth.py new file mode 100644 index 0000000..4afeb3a --- /dev/null +++ b/src/services/sysauth.py @@ -0,0 +1,350 @@ +"""Polkit authentication agent service for Modus. + +Registers as a Polkit authentication agent on the D-Bus session bus +and listens for authentication requests from polkitd. Emits signals +so the UI layer can show a password dialog. + +D-Bus flow: +1. Export org.freedesktop.PolicyKit1.AuthenticationAgent on session bus +2. Register with Polkit Authority on system bus +3. polkitd calls BeginAuthentication → service emits signal +4. UI responds via respond(cookie, password) +""" + +import os + +from fabric.core.service import Service, Signal +from fabric.utils import Gio, GLib, logger + +POLKIT_BUS_NAME = "org.freedesktop.PolicyKit1" +POLKIT_AUTHORITY_PATH = "/org/freedesktop/PolicyKit1/Authority" +POLKIT_AUTHORITY_IFACE = "org.freedesktop.PolicyKit1.Authority" + +AGENT_OBJECT_PATH = "/org/freedesktop/PolicyKit1/AuthenticationAgent" +AGENT_INTERFACE = "org.freedesktop.PolicyKit1.AuthenticationAgent" + +AGENT_INTROSPECTION = """ + + + + + + + + + + + + + + + +""" + + +class SysauthService(Service): + """Polkit authentication agent as a Modus service. + + Registers with polkitd and emits signals when authentication + is requested. The UI layer connects to these signals to show + a password dialog. + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @Signal + def begin_authentication( + self, action_id: str, message: str, icon_name: str, cookie: str, uid: int + ) -> None: + """Emitted when polkitd requests authentication. + + Args: + action_id: Processed action identifier (human-readable). + message: Description of the action requiring auth. + icon_name: Icon name for the action. + cookie: Session cookie for responding. + uid: User ID being authenticated. + """ + + @Signal + def authentication_cancelled(self, cookie: str) -> None: + """Emitted when polkitd cancels an authentication request.""" + + @Signal + def authentication_completed(self, cookie: str, success: bool) -> None: + """Emitted when authentication completes (success or failure).""" + + def __init__(self, **kwargs): + if getattr(self, "_initialized", False): + return + self._initialized = True + super().__init__(**kwargs) + + self._session_bus_id = None + self._authority_proxy = None + self._registered = False + + def start(self) -> bool: + """Start the agent: export on session bus and register with Authority. + + Returns: + True if registration succeeded, False otherwise. + """ + try: + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + if bus is None: + logger.error("[SysauthService] Cannot get session bus") + return False + + node_info = Gio.DBusNodeInfo.new_for_xml(AGENT_INTROSPECTION) + interface_info = node_info.lookup_interface(AGENT_INTERFACE) + + self._session_bus_id = bus.register_object_with_closures2( + AGENT_OBJECT_PATH, + interface_info, + self._handle_method_call, + ) + + logger.info( + f"[SysauthService] Exported agent on session bus at {AGENT_OBJECT_PATH}" + ) + + if not self._register_with_authority(): + logger.warning( + "[SysauthService] Failed to register with Authority, " + "running without polkit integration" + ) + return False + + return True + + except GLib.Error as e: + logger.error(f"[SysauthService] Failed to start: {e}") + return False + + def stop(self) -> None: + """Unregister and clean up D-Bus resources.""" + if self._session_bus_id is not None: + try: + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + if bus is not None: + bus.unregister_object(self._session_bus_id) + except GLib.Error: + pass + self._session_bus_id = None + + if self._registered: + self._unregister_from_authority() + self._registered = False + + logger.info("[SysauthService] Stopped") + + def respond(self, cookie: str, password: str) -> None: + """Send authentication response back to polkitd. + + Args: + cookie: The authentication session cookie. + password: The password to send. + """ + try: + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + if bus is None: + logger.error("[SysauthService] Cannot get session bus for response") + return + + bus.call_sync( + POLKIT_BUS_NAME, + AGENT_OBJECT_PATH, + AGENT_INTERFACE, + "Respond", + GLib.Variant("(ss)", (cookie, password)), + None, + Gio.DBusCallFlags.NONE, + -1, + None, + ) + except GLib.Error as e: + logger.error(f"[SysauthService] Failed to send response: {e}") + + def _register_with_authority(self) -> bool: + """Register this agent with the Polkit Authority on the system bus.""" + try: + system_bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) + if system_bus is None: + logger.error("[SysauthService] Cannot get system bus") + return False + + self._authority_proxy = Gio.DBusProxy.new_sync( + system_bus, + Gio.DBusProxyFlags.NONE, + None, + POLKIT_BUS_NAME, + POLKIT_AUTHORITY_PATH, + POLKIT_AUTHORITY_IFACE, + None, + ) + + pid = os.getpid() + locale = os.environ.get("LANG", "en_US.UTF-8") + + builder = GLib.VariantBuilder(GLib.VariantType("(sa{sv}so)")) + builder.add_value(GLib.Variant("s", "unix-process")) + details = GLib.VariantBuilder(GLib.VariantType("a{sv}")) + details.add_value(GLib.Variant("{sv}", ("pid", GLib.Variant("u", pid)))) + builder.add_value(details.end()) + builder.add_value(GLib.Variant("s", locale)) + builder.add_value(GLib.Variant("o", AGENT_OBJECT_PATH)) + + self._authority_proxy.call_sync( + "RegisterAuthenticationAgent", + builder.end(), + Gio.DBusCallFlags.NONE, + -1, + None, + ) + + self._registered = True + logger.info(f"[SysauthService] Registered with Authority (PID {pid})") + return True + + except GLib.Error as e: + logger.error(f"[SysauthService] Registration failed: {e}") + return False + + def _unregister_from_authority(self) -> None: + """Unregister this agent from the Polkit Authority.""" + if self._authority_proxy is None: + return + + try: + pid = os.getpid() + + builder = GLib.VariantBuilder(GLib.VariantType("(sa{sv}o)")) + builder.add_value(GLib.Variant("s", "unix-process")) + details = GLib.VariantBuilder(GLib.VariantType("a{sv}")) + details.add_value(GLib.Variant("{sv}", ("pid", GLib.Variant("u", pid)))) + builder.add_value(details.end()) + builder.add_value(GLib.Variant("o", AGENT_OBJECT_PATH)) + + self._authority_proxy.call_sync( + "UnregisterAuthenticationAgent", + builder.end(), + Gio.DBusCallFlags.NONE, + -1, + None, + ) + + logger.info("[SysauthService] Unregistered from Authority") + except GLib.Error as e: + logger.warning(f"[SysauthService] Unregister failed: {e}") + + def _handle_method_call( + self, + connection: Gio.DBusConnection, + sender: str, + object_path: str, + interface_name: str, + method_name: str, + parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation, + ) -> None: + """Handle incoming D-Bus method calls from polkitd.""" + if method_name == "BeginAuthentication": + self._handle_begin_authentication(parameters, invocation) + elif method_name == "CancelAuthentication": + self._handle_cancel_authentication(parameters, invocation) + else: + invocation.return_error_literal( + Gio.DBusError, + Gio.DBusError.UNKNOWN_METHOD, + f"Unknown method: {method_name}", + ) + + def _handle_begin_authentication( + self, + parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation, + ) -> None: + """Handle BeginAuthentication call from polkitd.""" + action_id, message, icon_name, _details, cookie, identities = ( + parameters.unpack() + ) + + uid = -1 + if identities: + for kind, value in identities: + if kind == "unix-user": + if isinstance(value, dict): + uid = value.get("uid", -1) + elif isinstance(value, int): + uid = value + elif isinstance(value, tuple): + uid = value[0] if value else -1 + break + + if uid == -1: + uid = os.getuid() + + processed_action = _process_action_id(action_id, message) + + logger.info( + f"[SysauthService] BeginAuthentication: action={processed_action} " + f"uid={uid} cookie={cookie[:16]}..." + ) + + invocation.return_value(None) + + GLib.idle_add( + lambda: self.emit( + "begin-authentication", + processed_action, + message, + icon_name, + cookie, + uid, + ) + ) + + def _handle_cancel_authentication( + self, + parameters: GLib.Variant, + invocation: Gio.DBusMethodInvocation, + ) -> None: + """Handle CancelAuthentication call from polkitd.""" + (cookie,) = parameters.unpack() + logger.info(f"[SysauthService] CancelAuthentication: cookie={cookie[:16]}") + invocation.return_value(None) + GLib.idle_add(lambda: self.emit("authentication-cancelled", cookie)) + + +def _process_action_id(action_id: str, message: str) -> str: + """Process action_id to extract a human-readable name. + + Matches the C++ sysauth behavior: + - For org.freedesktop.policykit.exec: extracts program name from message + - For other actions: strips everything before the last dot + """ + if action_id == "org.freedesktop.policykit.exec": + try: + start = message.rfind("/") + 1 + end = message.find("'", start) + if start > 0 and end > start: + return message[start:end] + except (ValueError, IndexError): + pass + return action_id + + if "." in action_id: + return action_id.rsplit(".", 1)[-1] + + return action_id + + +def get_sysauth_service() -> SysauthService: + """Get the singleton sysauth service instance.""" + return SysauthService() diff --git a/src/shared/dialogs/sysauth_dialog.py b/src/shared/dialogs/sysauth_dialog.py new file mode 100644 index 0000000..8043215 --- /dev/null +++ b/src/shared/dialogs/sysauth_dialog.py @@ -0,0 +1,277 @@ +"""Fabric-based authentication dialog for sysauth. + +Provides a centered overlay password prompt matching the WiFi dialog pattern. +Uses Fabric widgets and integrates with the SysauthService for +Polkit authentication requests. +""" + +import threading + +from fabric.utils import Gdk, GLib +from fabric.widgets.box import Box +from fabric.widgets.button import Button +from fabric.widgets.entry import Entry +from fabric.widgets.image import Image +from fabric.widgets.label import Label +from fabric.widgets.window import Window + + +class AuthDialog(Window): + """Centered authentication dialog matching the WiFi password dialog pattern.""" + + def __init__(self, action_id: str, message: str, icon_name: str): + self._result_accepted = False + self._main_loop = None + + super().__init__( + title="modus-dialog", + layer="overlay", + anchor="center", + keyboard_mode="on-demand", + visible=False, + name="sysauth-dialog", + ) + + self.set_type_hint(Gdk.WindowTypeHint.DIALOG) + self.set_keep_above(True) + self.set_modal(True) + self.set_skip_taskbar_hint(True) + self.set_skip_pager_hint(True) + self.set_resizable(False) + + self._action_id = action_id + self._message = message + self._icon_name = icon_name + + self._build_ui() + self._connect_signals() + + def _build_ui(self): + """Construct the dialog UI matching the WiFi dialog layout.""" + self._image = Image( + icon_name=self._icon_name, + size=48, + name="sysauth-icon", + ) + + self._label_title = Label( + label=self._action_id, + name="sysauth-title", + h_align="start", + wrap=True, + max_width_chars=40, + ) + + self._title_container = Box( + orientation="h", + spacing=8, + children=[self._image, self._label_title], + name="sysauth-title-container", + h_align="start", + ) + + self._label_description = Label( + label=self._message, + name="sysauth-description", + h_align="start", + wrap=True, + max_width_chars=40, + lines=3, + ) + + self._error_label = Label( + label="Incorrect password. Please try again.", + name="sysauth-error", + h_align="center", + visible=False, + ) + + self._password_label = Label( + label="Password:", + name="sysauth-password-label", + h_align="start", + ) + + self._entry_password = Entry( + placeholder_text="Enter password", + name="sysauth-password-entry", + visibility=False, + h_expand=True, + h_align="fill", + ) + + self._password_container = Box( + orientation="v", + h_expand=True, + spacing=6, + children=[self._password_label, self._entry_password], + name="sysauth-password-container", + ) + + self._button_cancel = Button( + label="Cancel", + name="sysauth-cancel-button", + on_clicked=self._on_cancel_clicked, + ) + + self._button_ok = Button( + label="Authenticate", + name="sysauth-ok-button", + on_clicked=self._on_ok_clicked, + ) + + self._button_box = Box( + orientation="h", + spacing=12, + h_expand=True, + children=[self._button_cancel, self._button_ok], + name="sysauth-button-box", + h_align="end", + ) + + self._content_box = Box( + orientation="v", + h_expand=True, + spacing=12, + children=[ + self._title_container, + self._label_description, + self._error_label, + self._password_container, + self._button_box, + ], + name="sysauth-content", + h_align="fill", + v_align="center", + ) + + self._dialog_background = Box( + children=[self._content_box], + name="sysauth-dialog-background", + h_align="center", + v_align="center", + ) + + self.children = self._dialog_background + + def _connect_signals(self): + """Connect button and entry signals.""" + self._entry_password.connect("activate", lambda *_: self._on_ok_clicked()) + self.connect("key-press-event", self._on_key_press) + self.connect("notify::visible", self._on_visibility_changed) + + def run(self) -> bool: + """Show the dialog and run a nested main loop until dismissed. + + Returns: + True if the user accepted (entered password), False if cancelled. + """ + self._main_loop = GLib.MainLoop() + + self.show_all() + self._error_label.set_visible(False) + self._entry_password.grab_focus() + + self._main_loop.run() + + return self._result_accepted + + def _on_key_press(self, _widget, event): + """Handle keyboard input.""" + keyval = event.keyval + + if keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter: + self._on_ok_clicked() + return True + elif keyval == Gdk.KEY_Escape: + self._on_cancel_clicked() + return True + + return False + + def _on_visibility_changed(self, _widget, *_args): + """Focus password entry when dialog becomes visible.""" + if self.get_visible(): + GLib.timeout_add(100, lambda: self._entry_password.grab_focus()) + + def _on_ok_clicked(self, *_args): + """Handle OK button press or Enter key.""" + password = self._entry_password.get_text().strip() + if not password: + self._entry_password.grab_focus() + return + + self._result_accepted = True + self._dismiss() + + def _on_cancel_clicked(self, *_args): + """Handle Cancel button press or Escape key.""" + self._result_accepted = False + self._dismiss() + + def _dismiss(self): + """Dismiss the dialog.""" + self.hide() + GLib.timeout_add(100, self._quit_main_loop) + + def _quit_main_loop(self): + """Quit the nested main loop.""" + if self._main_loop is not None and self._main_loop.is_running(): + self._main_loop.quit() + return False + + def show_error(self, message: str = "Incorrect password. Please try again."): + """Show an error message in the dialog.""" + self._error_label.set_text(message) + self._error_label.set_visible(True) + self._entry_password.set_text("") + GLib.timeout_add(10, lambda: self._entry_password.grab_focus()) + + def reset(self): + """Reset the dialog for reuse.""" + self._result_accepted = False + self._error_label.set_visible(False) + self._entry_password.set_text("") + + @property + def password(self) -> str: + """Get the entered password.""" + return self._entry_password.get_text() + + +def run_auth_dialog( + action_id: str, + message: str, + icon_name: str, + on_result=None, + error_message: "str | None" = None, +): + """Run an authentication dialog in a background thread. + + Args: + action_id: The action identifier to display. + message: Description of the action. + icon_name: Icon name for the action. + on_result: Callback(password: str | None) when dialog closes. + error_message: Optional error message to show on first display. + """ + + def _run(): + dialog = AuthDialog(action_id, message, icon_name) + + if error_message: + GLib.idle_add(lambda: dialog.show_error(error_message)) + + accepted = dialog.run() + + def _callback(): + if accepted and dialog.password: + on_result(dialog.password) + else: + on_result(None) + return False + + GLib.idle_add(_callback) + + thread = threading.Thread(target=_run, daemon=True) + thread.start() diff --git a/src/shared/dialogs/wifi_password_dialog.py b/src/shared/dialogs/wifi_password_dialog.py deleted file mode 100644 index fa87edd..0000000 --- a/src/shared/dialogs/wifi_password_dialog.py +++ /dev/null @@ -1,269 +0,0 @@ -from fabric.utils import Gdk, GLib, logger -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.image import Image -from fabric.widgets.label import Label -from fabric.widgets.window import Window - -from utils.gtk_utils import svg_file - - -class WiFiPasswordDialog(Window): - def __init__( - self, - ssid: str, - on_connect_callback=None, - on_cancel_callback=None, - on_dialog_closed=None, - **kwargs, - ): - super().__init__( - title="modus-dialog", - layer="overlay", - anchor="center", - keyboard_mode="on-demand", - visible=False, - name="wifi-password-dialog", - **kwargs, - ) - - self.ssid = ssid - self.on_connect_callback = on_connect_callback - self.on_cancel_callback = on_cancel_callback - # self.set_size_request(400, 300) - self.set_resizable(False) - self.on_dialog_closed = on_dialog_closed - self.is_connecting = False - - self._create_dialog_content() - self.connect("key-press-event", self._on_key_press) - self.connect("notify::visible", self._on_visibility_changed) - - def _create_dialog_content(self): - self.wifi_icon = svg_file( - "wifi/network-wireless-100.svg", size=20, name="wifi-dialog-icon" - ) - - self.title_label = Label( - label=f'The Wi-Fi network "{self.ssid}" requires a password.', - name="wifi-dialog-title", - h_align="start", - wrap=True, - max_width_chars=40, - ) - - self.title_container = Box( - orientation="h", - spacing=8, - children=[self.wifi_icon, self.title_label], - name="wifi-dialog-title-container", - h_align="start", - ) - - self.error_label = Label( - label="Incorrect password. Please try again.", - name="wifi-dialog-error", - h_align="center", - visible=False, - ) - - self.password_label = Label( - label="Password:", name="wifi-dialog-password-label", h_align="start" - ) - - self.password_entry = Entry( - placeholder_text="Enter password", - name="wifi-dialog-password-entry", - visibility=False, - h_expand=True, - h_align="fill", - ) - - self.password_entry.connect("activate", lambda *_: self._on_join_clicked()) - self.password_entry.connect("changed", self._on_password_changed) - - self.password_visible = False - self.show_password_button = Button( - image=Image(icon_name="view-conceal-symbolic", size=16), - name="wifi-dialog-show-password-button", - on_clicked=self._on_show_password_clicked, - ) - - self.show_password_label = Label( - label="Show password", name="wifi-dialog-show-password-label" - ) - - self.show_password_box = Box( - orientation="h", - spacing=8, - children=[self.show_password_button, self.show_password_label], - name="wifi-dialog-show-password-box", - ) - - self.cancel_button = Button( - label="Cancel", - name="wifi-dialog-cancel-button", - on_clicked=self._on_cancel_clicked, - ) - - self.join_button = Button( - label="Join", - name="wifi-dialog-join-button", - on_clicked=self._on_join_clicked, - ) - - self.button_box = Box( - orientation="h", - spacing=12, - h_expand=True, - children=[self.cancel_button, self.join_button], - name="wifi-dialog-button-box", - h_align="end", - ) - - self.password_container = Box( - orientation="v", - h_expand=True, - spacing=6, - children=[self.password_label, self.password_entry, self.show_password_box], - name="wifi-dialog-password-container", - ) - - self.content_box = Box( - orientation="v", - h_expand=True, - spacing=12, - children=[ - self.title_container, - self.error_label, - self.password_container, - self.button_box, - ], - name="wifi-dialog-content", - h_align="fill", - v_align="center", - ) - - self.dialog_background = Box( - children=[self.content_box], - name="wifi-dialog-background", - h_align="center", - v_align="center", - ) - - self.children = self.dialog_background - - self._update_join_button_state() - - def _on_password_changed(self, entry): - self._update_join_button_state() - - def _update_join_button_state(self): - password = self.password_entry.get_text().strip() - has_password = len(password) > 0 - - if has_password: - self.join_button.set_opacity(1.0) - self.join_button.remove_style_class("disabled") - else: - self.join_button.set_opacity(0.5) - self.join_button.add_style_class("disabled") - - def _on_show_password_clicked(self, *args): - self.password_visible = not self.password_visible - self.password_entry.set_visibility(self.password_visible) - - icon_name = ( - "view-reveal-symbolic" if self.password_visible else "view-conceal-symbolic" - ) - self.show_password_button.get_image().set_property("icon-name", icon_name) - - def _on_key_press(self, widget, event): - keyval = event.keyval - - if keyval == Gdk.KEY_Return or keyval == Gdk.KEY_KP_Enter: - self._on_join_clicked() - return True - elif keyval == Gdk.KEY_Escape: - self._on_cancel_clicked() - return True - - return False - - def _on_visibility_changed(self, widget, *args): - """Handle visibility changes""" - if self.get_visible(): - GLib.timeout_add(100, lambda: self.password_entry.grab_focus()) - - def _on_cancel_clicked(self, *args): - self.hide() - if self.on_cancel_callback: - self.on_cancel_callback() - if self.on_dialog_closed: - self.on_dialog_closed() - - def _on_join_clicked(self, *args): - if self.is_connecting: - return - - password = self.password_entry.get_text().strip() - if not password: - self.password_entry.grab_focus() - return - - self.is_connecting = True - self.join_button.set_sensitive(False) - - self.error_label.set_visible(False) - - self.hide() - if self.on_connect_callback: - self.on_connect_callback(self.ssid, password) - if self.on_dialog_closed: - self.on_dialog_closed() - - def show_dialog(self): - self.show_all() - self.password_entry.set_text("") - self.error_label.set_visible(False) - - self.password_visible = False - self.password_entry.set_visibility(False) - self.show_password_button.get_image().set_property( - "icon-name", "view-conceal-symbolic" - ) - - self.is_connecting = False - self.join_button.set_sensitive(True) - - self._update_join_button_state() - - def show_error(self, message="Incorrect password. Please try again."): - self.is_connecting = False - self.join_button.set_sensitive(True) - - if not self.get_visible(): - self.error_label.set_text(message) - self.error_label.set_visible(True) - self.show_all() - GLib.timeout_add(10, lambda: self._focus_and_select_password()) - else: - self.error_label.set_text(message) - self.error_label.set_visible(True) - self._focus_and_select_password() - - def _focus_and_select_password(self): - try: - self.password_entry.grab_focus() - self.password_entry.select_region(0, -1) - return False - except Exception as e: - logger.warning( - f"[wifi_password_dialog] self.password_entry.grab_focus() failed: {e}" - ) - return False - - def destroy_dialog(self): - self.hide() - self.destroy() diff --git a/src/styles/main.css b/src/styles/main.css index df55fcd..f5d4053 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -15,3 +15,4 @@ @import url("widgets/pomodoro.css"); @import url("widgets/todo.css"); +@import url("widgets/sysauth.css"); diff --git a/src/styles/widgets/sysauth.css b/src/styles/widgets/sysauth.css new file mode 100644 index 0000000..0d40b1d --- /dev/null +++ b/src/styles/widgets/sysauth.css @@ -0,0 +1,98 @@ +/* Sysauth Dialog */ + +#sysauth-dialog { + background-color: transparent; +} + +#sysauth-dialog-background { + background-color: @glass-dialog; + border-radius: apply(radius-md); + border: 1px solid @glass-border; + padding: 20px; + min-width: 400px; +} + +#sysauth-title-container { + margin-bottom: 8px; +} + +#sysauth-icon { + color: @glass-accent; + margin-right: 4px; +} + +#sysauth-title { + color: @glass-text; + font-size: apply(font-base); + font-weight: 500; +} + +#sysauth-description { + color: @glass-text-secondary; + font-size: apply(font-sm); + margin-bottom: 8px; +} + +#sysauth-error { + color: @glass-error; + font-size: apply(font-sm); + font-weight: 500; + margin-bottom: 8px; +} + +#sysauth-password-label { + color: @glass-text; + font-size: apply(font-md); + font-weight: 500; + margin-bottom: 4px; +} + +#sysauth-password-entry { + background-color: @glass-surface-1; + border: 1px solid @glass-border; + border-radius: apply(radius-xs); + padding: 8px 12px; + color: @glass-text; + font-size: apply(font-md); +} + +#sysauth-password-entry:focus { + border-color: @glass-ring; + box-shadow: 0 0 0 2px @glass-focus; + background-color: @glass-hover; +} + +#sysauth-button-box { + margin-top: 8px; +} + +#sysauth-cancel-button, +#sysauth-ok-button { + border-radius: apply(radius-xs); + font-size: apply(font-md); + font-weight: 500; + min-width: 80px; + min-height: 30px; + padding: 6px 12px; +} + +#sysauth-cancel-button { + background-color: @glass-hover; + border: 1px solid @glass-border; + color: @glass-text; +} + +#sysauth-cancel-button:hover { + background-color: @glass-active; +} + +#sysauth-ok-button { + background-color: @glass-accent; + border: 1px solid @glass-accent; + color: @glass-text; +} + +#sysauth-ok-button:hover { + background-color: @glass-accent; + border-color: @glass-accent; +} diff --git a/src/utils/password.py b/src/utils/password.py new file mode 100644 index 0000000..62f7a83 --- /dev/null +++ b/src/utils/password.py @@ -0,0 +1,137 @@ +"""Password verification and keyring caching utilities. + +Provides PAM-based password verification with su fallback, +and system keyring integration for caching authenticated passwords. +""" + +import subprocess + +from fabric.utils import logger + +KEYRING_SERVICE = "sysauth" + + +def verify_password(username: str, password: str) -> bool: + """Verify password using PAM with su fallback. + + Args: + username: The username to authenticate. + password: The password to verify. + + Returns: + True if authentication succeeded, False otherwise. + """ + if _verify_pam(username, password): + return True + return _verify_su(username, password) + + +def _verify_pam(username: str, password: str) -> bool: + """Verify password using python-pam.""" + try: + import pam as pam_module + + p = pam_module.pam() + return p.authenticate(username, password) + except ImportError: + return False + except Exception as e: + logger.warning(f"[password] PAM verification failed: {e}") + return False + + +def _verify_su(username: str, password: str) -> bool: + """Verify password using the su command as fallback.""" + try: + result = subprocess.run( + ["su", "-c", "true", username], + input=password.encode(), + capture_output=True, + timeout=5, + ) + return result.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e: + logger.warning(f"[password] su verification failed: {e}") + return False + + +def cache_password(action_id: str, uid: int, password: str) -> None: + """Cache a password in the system keyring. + + Args: + action_id: The action identifier. + uid: The user ID being authenticated. + """ + kr = _get_keyring() + if kr is None: + return + + try: + cache_key = f"{action_id}:{uid}" + kr.set_password(KEYRING_SERVICE, cache_key, password) + logger.debug(f"[password] Cached password for {action_id} (uid={uid})") + except Exception as e: + logger.debug(f"[password] Failed to cache password: {e}") + + +def get_cached_password(action_id: str, uid: int) -> "str | None": + """Retrieve a cached password from the system keyring. + + Args: + action_id: The action identifier. + uid: The user ID being authenticated. + + Returns: + The cached password, or None if not found. + """ + kr = _get_keyring() + if kr is None: + return None + + try: + cache_key = f"{action_id}:{uid}" + password = kr.get_password(KEYRING_SERVICE, cache_key) + if password: + logger.debug( + f"[password] Retrieved cached password for {action_id} (uid={uid})" + ) + return password + except Exception as e: + logger.debug(f"[password] Failed to retrieve cached password: {e}") + return None + + +def get_username_from_uid(uid: int) -> "str": + """Get the username for a given UID. + + Args: + uid: The user ID. + + Returns: + The username, or 'root' if lookup fails. + """ + try: + import pwd + + return pwd.getpwuid(uid).pw_name + except (KeyError, ImportError): + return "root" + + +def _get_keyring(): + """Get the keyring backend, using keyrings.alt file-based backend.""" + try: + import keyring as keyring_module + + try: + from keyrings.alt.file import PlaintextKeyring + + kb = PlaintextKeyring() + keyring_module.set_keyring(kb) + except ImportError: + pass + + return keyring_module + except ImportError: + logger.debug("[password] keyring module not available") + return None diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index ba895b9..8e835d5 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -7,7 +7,7 @@ from fabric.widgets.label import Label from fabric.widgets.separator import Separator from services.network import NetworkClient -from shared.dialogs.wifi_password_dialog import WiFiPasswordDialog +from shared.dialogs.sysauth_dialog import run_auth_dialog from shared.widgets.smooth_switch import SmoothSwitch from shared.window.animated_scrollwindow import AnimatedScrollable from utils.functions import ( @@ -62,8 +62,6 @@ class WifiNetworkSlot(Box): name="wifi-lock-icon", ) - self.password_dialog = None - start_box = Box( orientation="h", spacing=8, @@ -184,19 +182,19 @@ class WifiNetworkSlot(Box): if self.parent and hasattr(self.parent, "hide_controlcenter"): self.parent.hide_controlcenter() - if self.password_dialog: - self.password_dialog.destroy_dialog() - - self.password_dialog = WiFiPasswordDialog( - ssid=self.ssid, - on_connect_callback=self._on_password_connect, - on_cancel_callback=self._on_password_cancel, + run_auth_dialog( + action_id=self.ssid, + message=f'Wi-Fi network "{self.ssid}" requires a password.', + icon_name="network-wireless-symbolic", + on_result=self._on_password_result, ) - self.password_dialog.show_dialog() + def _on_password_result(self, password): + """Handle password dialog result""" + if password is None: + self._reset_connect_state() + return - def _on_password_connect(self, ssid, password): - """Handle password dialog connect action""" if password.strip(): connecting_icon = get_wifi_connecting_icon() self.dimage.set_from_file(connecting_icon) @@ -209,15 +207,10 @@ class WifiNetworkSlot(Box): return if success: self.is_connected = True - GLib.timeout_add(500, lambda: self._reset_connect_state()) - - if self.password_dialog: - self.password_dialog.is_connecting = False else: self._reset_connect_state() - if self.password_dialog: - self._show_connection_error(message) + self._show_connection_error(message) self.on_changed() @@ -231,20 +224,20 @@ class WifiNetworkSlot(Box): f"[WiFi] Failed to initiate connection to {self.access_point.ssid}: {e}" ) self._reset_connect_state() - if self.password_dialog: - self._show_connection_error("Connection failed. Please try again.") + self._show_connection_error("Connection failed. Please try again.") self.on_changed() def _show_connection_error(self, message="Incorrect password. Please try again."): - """Show connection error in a separate thread to prevent UI blocking""" - if self.password_dialog: - self.password_dialog.show_error(message) + """Show connection error and re-prompt for password""" + run_auth_dialog( + action_id=self.ssid, + message=f'Wi-Fi network "{self.ssid}" requires a password.', + icon_name="network-wireless-symbolic", + on_result=self._on_password_result, + error_message=message, + ) return False # Don't repeat if called from GLib.timeout_add - def _on_password_cancel(self): - """Handle password dialog cancel action""" - self._reset_connect_state() - class WifiConnections(Box): def __init__(self, parent, show_back_button=True, network_service=None, **kwargs): diff --git a/uv.lock b/uv.lock index 92f2b2b..df2b1e7 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -53,6 +101,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, +] + [[package]] name = "distlib" version = "0.4.3" @@ -137,6 +229,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "keyrings-alt" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/7b/e3bf53326e0753bee11813337b1391179582ba5c6851b13e0d9502d15a50/keyrings_alt-5.0.2.tar.gz", hash = "sha256:8f097ebe9dc8b185106502b8cdb066c926d2180e13b4689fd4771a3eab7d69fb", size = 29229, upload-time = "2024-08-14T01:09:28.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/0d/9c59313ab43d0858a9a665e80763bd830dc78d5f379afc3815e123c486c2/keyrings.alt-5.0.2-py3-none-any.whl", hash = "sha256:6be74693192f3f37bbb752bfac9b86e6177076b17d2ac12a390f1d6abff8ac7c", size = 17930, upload-time = "2024-08-14T01:09:26.785Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -157,6 +321,8 @@ source = { virtual = "." } dependencies = [ { name = "fabric" }, { name = "httpx" }, + { name = "keyring" }, + { name = "keyrings-alt" }, { name = "psutil" }, { name = "python-pam" }, { name = "tomlkit" }, @@ -172,6 +338,8 @@ dev = [ requires-dist = [ { name = "fabric", git = "https://github.com/Fabric-Development/fabric.git" }, { name = "httpx", specifier = ">=0.28.1" }, + { name = "keyring", specifier = ">=23.0.0" }, + { name = "keyrings-alt", specifier = ">=5.0.2" }, { name = "psutil", specifier = ">=7.2.2" }, { name = "python-pam", specifier = ">=2.0.2" }, { name = "tomlkit", specifier = ">=0.15.0" }, @@ -183,6 +351,15 @@ dev = [ { name = "ruff", specifier = ">=0.15.2" }, ] +[[package]] +name = "more-itertools" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -261,6 +438,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/54/123f6239685f5f3f2edc123f1e38d2eefacebee18cf3c532d2f4bd51d0ef/pycairo-1.29.0-cp314-cp314t-win_arm64.whl", hash = "sha256:caba0837a4b40d47c8dfb0f24cccc12c7831e3dd450837f2a356c75f21ce5a15", size = 721404, upload-time = "2025-11-11T19:12:36.919Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygobject" version = "3.50.0" @@ -292,6 +478,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/2d/9fbb3bd686a474d76fbd0b79abdcc016f3da760b1d1c2048bf4c611a4939/python_pam-2.0.2-py3-none-any.whl", hash = "sha256:4ac51dd8953ac59aa45505882b565eef6a22e0423dcf25d63369902080416c20", size = 10658, upload-time = "2022-03-18T00:32:07.802Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -353,6 +548,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + [[package]] name = "tomlkit" version = "0.15.1"