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
This commit is contained in:
sankalp committed 2026-07-26 15:28:09 +05:45
1 parent 7917eea54b
commit eba5827189
11 files changed
+1100 -297

No files matched your search

+2
View File
@@ -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",
+5
View File
@@ -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()
+1
View File
@@ -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
+350
View File
@@ -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 = """<!DOCTYPE node>
<node>
<interface name="org.freedesktop.PolicyKit1.AuthenticationAgent">
<method name="BeginAuthentication">
<arg name="action_id" type="s" direction="in"/>
<arg name="message" type="s" direction="in"/>
<arg name="icon_name" type="s" direction="in"/>
<arg name="details" type="a{ss}" direction="in"/>
<arg name="cookie" type="s" direction="in"/>
<arg name="identities" type="a(si)" direction="in"/>
</method>
<method name="CancelAuthentication">
<arg name="cookie" type="s" direction="in"/>
</method>
</interface>
</node>
"""
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()
+277
View File
@@ -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()
-269
View File
@@ -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()
+1
View File
@@ -15,3 +15,4 @@
@import url("widgets/pomodoro.css");
@import url("widgets/todo.css");
@import url("widgets/sysauth.css");
+98
View File
@@ -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;
}
+137
View File
@@ -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
+21 -28
View File
@@ -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):
Generated
+208
View File
@@ -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"