diff --git a/src/openadapt_tray/app.py b/src/openadapt_tray/app.py index 8f944ff..3b6fb6c 100644 --- a/src/openadapt_tray/app.py +++ b/src/openadapt_tray/app.py @@ -9,7 +9,6 @@ * the desktop app is the source of truth for all state โ€” the tray renders it. """ -import subprocess import sys import threading import webbrowser @@ -17,6 +16,7 @@ import pystray from openadapt_tray.config import ConfigLoadError, TrayConfig +from openadapt_tray.desktop import DesktopLaunchError, launch_native_desktop from openadapt_tray.hosted import ( CountResult, HostedPoller, @@ -40,9 +40,6 @@ TrayState, ) -# How the tray launches the desktop app when the socket is unreachable. -DESKTOP_APP_COMMAND = "openadapt-desktop" - class TrayApplication: """Main system tray application.""" @@ -243,7 +240,8 @@ def ensure_desktop_connection(self) -> bool: return True # Desktop not running โ€” launch it, then poll for the discovery file. - self._launch_desktop_app() + if not self._launch_desktop_app(): + return False for _ in range(20): # ~10s if self.ipc.refresh_from_discovery() and self.ipc.connect(): return True @@ -251,21 +249,21 @@ def ensure_desktop_connection(self) -> bool: return False - def _launch_desktop_app(self) -> None: - """Spawn the desktop app process (best-effort).""" + def _launch_desktop_app(self) -> bool: + """Launch the installed native desktop application.""" try: - subprocess.Popen( - [DESKTOP_APP_COMMAND], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except FileNotFoundError: + launch_native_desktop() + except DesktopLaunchError as e: self.notifications.show( "Desktop app not found", "Install the OpenAdapt desktop app to record and manage workflows.", ) + print(f"Failed to launch desktop app: {e}") + return False except Exception as e: print(f"Failed to launch desktop app: {e}") + return False + return True # --- recording actions (delegated to the desktop over IPC) -------------- @@ -484,7 +482,7 @@ def _on_ipc_recording_started(self, message) -> None: data = message.data or {} self.state.transition( TrayState.RECORDING, - current_capture=data.get("name"), + current_capture=data.get("capture_id") or data.get("name"), ) def _on_ipc_recording_stopped(self, message) -> None: @@ -502,8 +500,8 @@ def _on_ipc_recording_error(self, message) -> None: def _on_ipc_status_update(self, message) -> None: """Handle a full status update from the desktop (source of truth). - The payload may carry ``state``, ``deployment_lane``, ``sync_state``, - ``break_count`` and ``offline``. + Protocol v1 carries the canonical ``recording`` boolean and optional + ``capture_id``. Hosted status fields remain orthogonal. """ data = message.data or {} @@ -518,25 +516,49 @@ def _on_ipc_status_update(self, message) -> None: if "break_count" in data: self._apply_break_count(data.get("break_count")) + recording = data.get("recording") + if type(recording) is bool: + self.state.transition( + TrayState.RECORDING if recording else TrayState.IDLE, + current_capture=(data.get("capture_id") if recording else None), + ) + return + + # Compatibility with the pre-v1 status projection. Protocol v1 uses + # the canonical ``recording`` boolean above. state_name = data.get("state") - if state_name: + if isinstance(state_name, str): try: - self.state.transition(TrayState[state_name]) + self.state.transition(TrayState[state_name.upper()]) except KeyError: pass def _on_ipc_compile_progress(self, message) -> None: """Handle a compile-progress event (recording โ†’ workflow).""" data = message.data or {} - # Any progress signal means we are compiling; a terminal signal returns - # to idle. - if data.get("done"): + state = str(data.get("state") or "").lower() + capture_id = data.get("capture_id") or data.get("name") + if state == "compiled": self.state.transition(TrayState.IDLE) - else: + elif state in {"failed", "review_failed"}: + default_error = ( + "The action review could not open." + if state == "review_failed" + else "The recording could not be compiled." + ) + self.state.transition( + TrayState.ERROR, + current_capture=capture_id, + error_message=str(data.get("error") or default_error), + ) + elif state == "compiling": self.state.transition( TrayState.COMPILING, - current_capture=data.get("name"), + current_capture=capture_id, ) + elif data.get("done") is True: + # Compatibility with an older terminal projection. + self.state.transition(TrayState.IDLE) def _on_ipc_sync_state(self, message) -> None: """Handle a sync-state event from the desktop.""" diff --git a/src/openadapt_tray/desktop.py b/src/openadapt_tray/desktop.py new file mode 100644 index 0000000..b24ef98 --- /dev/null +++ b/src/openadapt_tray/desktop.py @@ -0,0 +1,135 @@ +"""Native OpenAdapt Desktop discovery and launch helpers.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +from collections.abc import Callable, Mapping +from pathlib import Path + +DESKTOP_APP_NAME = "OpenAdapt Desktop" +DESKTOP_BUNDLE_IDENTIFIER = "ai.openadapt.desktop" +LINUX_DESKTOP_IDENTITY = DESKTOP_APP_NAME +LINUX_NATIVE_EXECUTABLE = Path("/usr/bin/openadapt-desktop") + + +class DesktopLaunchError(RuntimeError): + """The installed native Desktop application could not be launched.""" + + +def _run_launcher( + command: list[str], + *, + runner: Callable[..., subprocess.CompletedProcess], +) -> bool: + """Run an OS launcher and return whether it accepted the application.""" + try: + result = runner( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=10, + check=False, + ) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 + + +def _spawn_executable( + executable: Path, + *, + spawner: Callable[..., subprocess.Popen], +) -> bool: + """Start an exact native executable path without resolving a CLI name.""" + try: + spawner( + [str(executable)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except OSError: + return False + return True + + +def _windows_candidates(environment: Mapping[str, str]) -> tuple[Path, ...]: + """Return the native paths produced by Desktop's MSI and NSIS installers.""" + candidates: list[Path] = [] + for variable in ("LOCALAPPDATA", "ProgramFiles", "ProgramFiles(x86)"): + root = environment.get(variable) + if root: + candidates.append(Path(root) / DESKTOP_APP_NAME / "openadapt-desktop.exe") + return tuple(candidates) + + +def launch_native_desktop( + *, + platform: str | None = None, + environment: Mapping[str, str] | None = None, + runner: Callable[..., subprocess.CompletedProcess] = subprocess.run, + spawner: Callable[..., subprocess.Popen] = subprocess.Popen, + finder: Callable[[str], str | None] = shutil.which, + is_file: Callable[[Path], bool] = Path.is_file, +) -> None: + """Launch the installed native Desktop application for the current OS. + + The function never resolves the Python ``openadapt-desktop`` console script. + It uses the macOS bundle identifier, a registered Linux desktop identity, + or an exact path produced by a Windows native installer. + """ + platform = platform or sys.platform + if environment is None: + environment = os.environ + + if platform == "darwin": + if _run_launcher( + ["open", "-b", DESKTOP_BUNDLE_IDENTIFIER], + runner=runner, + ): + return + raise DesktopLaunchError( + f"macOS could not open {DESKTOP_APP_NAME} ({DESKTOP_BUNDLE_IDENTIFIER})." + ) + + if platform == "win32": + for executable in _windows_candidates(environment): + if is_file(executable) and _spawn_executable( + executable, + spawner=spawner, + ): + return + raise DesktopLaunchError( + f"Windows could not find an installed {DESKTOP_APP_NAME} application." + ) + + if platform.startswith("linux"): + gtk_launch = finder("gtk-launch") + if gtk_launch and _run_launcher( + [gtk_launch, LINUX_DESKTOP_IDENTITY], + runner=runner, + ): + return + + if is_file(LINUX_NATIVE_EXECUTABLE) and _spawn_executable( + LINUX_NATIVE_EXECUTABLE, + spawner=spawner, + ): + return + + appimage = environment.get("OPENADAPT_DESKTOP_APPIMAGE") + if appimage: + executable = Path(appimage).expanduser() + if is_file(executable) and _spawn_executable( + executable, + spawner=spawner, + ): + return + + raise DesktopLaunchError( + f"Linux could not open the registered {DESKTOP_APP_NAME} application." + ) + + raise DesktopLaunchError(f"Desktop launch is not supported on {platform}.") diff --git a/src/openadapt_tray/ipc.py b/src/openadapt_tray/ipc.py index 7649838..a5e8305 100644 --- a/src/openadapt_tray/ipc.py +++ b/src/openadapt_tray/ipc.py @@ -23,6 +23,7 @@ # Discovery file the desktop app writes on startup (see ยง3d of the spec). DEFAULT_DISCOVERY_PATH = Path.home() / ".openadapt" / "desktop_ipc.json" +SUPPORTED_PROTOCOL_VERSION = 1 class IPCMessageType(Enum): @@ -85,14 +86,13 @@ def from_json(cls, json_str: str) -> "IPCMessage": class DesktopEndpoint: """Discovered desktop-app IPC endpoint.""" + protocol_version: int host: str port: int token: str | None = None @classmethod - def load( - cls, path: Path | None = None - ) -> Optional["DesktopEndpoint"]: + def load(cls, path: Path | None = None) -> Optional["DesktopEndpoint"]: """Load the desktop IPC endpoint from the discovery file. Args: @@ -108,10 +108,21 @@ def load( if not path.exists(): return None data = json.loads(path.read_text()) + protocol_version = data.get("protocol_version") + if ( + type(protocol_version) is not int + or protocol_version != SUPPORTED_PROTOCOL_VERSION + ): + print( + "Could not use desktop IPC discovery file: " + f"protocol_version must be {SUPPORTED_PROTOCOL_VERSION}" + ) + return None port = data.get("port") if port is None: return None return cls( + protocol_version=protocol_version, host=data.get("host", "127.0.0.1"), port=int(port), token=data.get("token"), @@ -150,9 +161,7 @@ def __init__( self._handlers: dict[IPCMessageType, Callable[[IPCMessage], None]] = {} @classmethod - def from_discovery( - cls, path: Path | None = None - ) -> Optional["IPCClient"]: + def from_discovery(cls, path: Path | None = None) -> Optional["IPCClient"]: """Build a client from the desktop discovery file, if present. Args: @@ -221,6 +230,13 @@ def connect(self) -> bool: ) self._listener_thread.start() + # The desktop is authoritative. Request a complete status snapshot + # before the tray accepts any user action based on its default + # in-memory state. + if not self.send_get_status(): + self.close() + return False + return True except OSError as e: print(f"IPC connection failed: {e}") diff --git a/tests/test_app.py b/tests/test_app.py index 591937d..95adf32 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -3,6 +3,8 @@ from pathlib import Path from unittest.mock import MagicMock, patch +import pytest + from openadapt_tray.config import ConfigLoadError, TrayConfig from openadapt_tray.platform.base import DialogUnavailableError from openadapt_tray.state import SyncState, TrayState @@ -443,6 +445,28 @@ def test_connected_desktop_command_failure_is_reported(self): app.notifications.show.assert_called_once() + def test_desktop_launch_uses_native_application_helper(self): + app = _make_test_app() + + with patch("openadapt_tray.app.launch_native_desktop") as launch: + assert app._launch_desktop_app() is True + + launch.assert_called_once_with() + + def test_missing_native_desktop_is_reported(self): + from openadapt_tray.desktop import DesktopLaunchError + + app = _make_test_app() + app.notifications = MagicMock() + + with patch( + "openadapt_tray.app.launch_native_desktop", + side_effect=DesktopLaunchError("not installed"), + ): + assert app._launch_desktop_app() is False + + app.notifications.show.assert_called_once() + def test_browser_failure_is_not_reported_as_opened(self): app = _make_test_app() app.notifications = MagicMock() @@ -520,3 +544,70 @@ def test_invalid_status_count_keeps_last_known_value(self): app._on_ipc_status_update(message) assert app.state.current.break_count == 4 + + +class TestCanonicalDesktopState: + """The tray consumes Desktop protocol v1 without compatibility fields.""" + + def test_status_uses_recording_boolean_and_capture_id(self): + app = _make_test_app() + + app._on_ipc_status_update( + MagicMock( + data={ + "recording": True, + "paused": False, + "capture_id": "capture-1", + } + ) + ) + + assert app.state.current.state == TrayState.RECORDING + assert app.state.current.current_capture == "capture-1" + + def test_status_false_clears_stale_recording_state(self): + app = _make_test_app() + app.state.transition(TrayState.RECORDING, current_capture="old") + + app._on_ipc_status_update( + MagicMock(data={"recording": False, "capture_id": None}) + ) + + assert app.state.current.state == TrayState.IDLE + assert app.state.current.current_capture is None + + def test_recording_event_uses_capture_id(self): + app = _make_test_app() + + app._on_ipc_recording_started(MagicMock(data={"capture_id": "capture-2"})) + + assert app.state.current.current_capture == "capture-2" + + def test_compiled_is_a_successful_terminal_state(self): + app = _make_test_app() + app.state.transition(TrayState.COMPILING, current_capture="capture-3") + + app._on_ipc_compile_progress( + MagicMock(data={"state": "compiled", "capture_id": "capture-3"}) + ) + + assert app.state.current.state == TrayState.IDLE + assert app.state.current.error_message is None + + @pytest.mark.parametrize("terminal", ["failed", "review_failed"]) + def test_compile_failure_terminal_states_stay_visible(self, terminal): + app = _make_test_app() + + app._on_ipc_compile_progress( + MagicMock( + data={ + "state": terminal, + "capture_id": "capture-4", + "error": "retained failure", + } + ) + ) + + assert app.state.current.state == TrayState.ERROR + assert app.state.current.current_capture == "capture-4" + assert app.state.current.error_message == "retained failure" diff --git a/tests/test_desktop.py b/tests/test_desktop.py new file mode 100644 index 0000000..cf6f631 --- /dev/null +++ b/tests/test_desktop.py @@ -0,0 +1,123 @@ +"""Tests for launching the installed native Desktop application.""" + +import subprocess +from pathlib import Path +from unittest.mock import MagicMock + +import pytest + +from openadapt_tray.desktop import ( + DESKTOP_BUNDLE_IDENTIFIER, + LINUX_NATIVE_EXECUTABLE, + DesktopLaunchError, + launch_native_desktop, +) + + +def _result(returncode: int) -> subprocess.CompletedProcess: + return subprocess.CompletedProcess([], returncode) + + +def test_macos_launches_by_bundle_identifier() -> None: + runner = MagicMock(return_value=_result(0)) + + launch_native_desktop(platform="darwin", runner=runner) + + assert runner.call_args.args[0] == [ + "open", + "-b", + DESKTOP_BUNDLE_IDENTIFIER, + ] + + +def test_macos_launcher_failure_is_visible() -> None: + runner = MagicMock(return_value=_result(1)) + + with pytest.raises(DesktopLaunchError, match="macOS could not open"): + launch_native_desktop(platform="darwin", runner=runner) + + +def test_windows_launches_an_exact_native_installer_path() -> None: + spawner = MagicMock() + expected = ( + Path(r"C:\Users\person\AppData\Local") + / "OpenAdapt Desktop" + / "openadapt-desktop.exe" + ) + + launch_native_desktop( + platform="win32", + environment={"LOCALAPPDATA": r"C:\Users\person\AppData\Local"}, + spawner=spawner, + is_file=lambda path: path == expected, + ) + + assert spawner.call_args.args[0] == [str(expected)] + + +def test_windows_never_falls_back_to_the_python_console_script() -> None: + with pytest.raises(DesktopLaunchError, match="Windows could not find"): + launch_native_desktop( + platform="win32", + environment={}, + is_file=lambda _path: False, + ) + + +def test_linux_launches_by_registered_desktop_identity() -> None: + runner = MagicMock(return_value=_result(0)) + + launch_native_desktop( + platform="linux", + environment={}, + runner=runner, + finder=lambda name: "/usr/bin/gtk-launch" if name == "gtk-launch" else None, + ) + + assert runner.call_args.args[0] == [ + "/usr/bin/gtk-launch", + "OpenAdapt Desktop", + ] + + +def test_linux_can_launch_the_exact_deb_executable() -> None: + spawner = MagicMock() + + launch_native_desktop( + platform="linux", + environment={}, + spawner=spawner, + finder=lambda _name: None, + is_file=lambda path: path == LINUX_NATIVE_EXECUTABLE, + ) + + assert spawner.call_args.args[0] == [str(LINUX_NATIVE_EXECUTABLE)] + + +def test_linux_can_launch_an_explicit_native_appimage() -> None: + spawner = MagicMock() + appimage = Path("/opt/OpenAdapt Desktop.AppImage") + + launch_native_desktop( + platform="linux", + environment={"OPENADAPT_DESKTOP_APPIMAGE": str(appimage)}, + spawner=spawner, + finder=lambda _name: None, + is_file=lambda path: path == appimage, + ) + + assert spawner.call_args.args[0] == [str(appimage)] + + +def test_unknown_platform_does_not_use_linux_launchers() -> None: + runner = MagicMock() + + with pytest.raises(DesktopLaunchError, match="not supported on freebsd"): + launch_native_desktop( + platform="freebsd", + environment={}, + runner=runner, + finder=lambda _name: "/usr/bin/gtk-launch", + ) + + runner.assert_not_called() diff --git a/tests/test_ipc.py b/tests/test_ipc.py index 66b611b..ca54049 100644 --- a/tests/test_ipc.py +++ b/tests/test_ipc.py @@ -1,9 +1,11 @@ """Tests for the desktop IPC client + discovery.""" import json +from unittest.mock import patch from openadapt_tray.ipc import ( DEFAULT_DISCOVERY_PATH, + SUPPORTED_PROTOCOL_VERSION, DesktopEndpoint, IPCClient, IPCMessage, @@ -90,11 +92,17 @@ def test_load_valid(self, tmp_path): f = tmp_path / "desktop_ipc.json" f.write_text( json.dumps( - {"host": "127.0.0.1", "port": 51234, "token": "sess-abc"} + { + "protocol_version": SUPPORTED_PROTOCOL_VERSION, + "host": "127.0.0.1", + "port": 51234, + "token": "sess-abc", + } ) ) ep = DesktopEndpoint.load(f) assert ep is not None + assert ep.protocol_version == SUPPORTED_PROTOCOL_VERSION assert ep.host == "127.0.0.1" assert ep.port == 51234 assert ep.token == "sess-abc" @@ -108,7 +116,27 @@ def test_load_invalid_json_returns_none(self, tmp_path): def test_load_without_port_returns_none(self, tmp_path): """A discovery file missing the port is unusable.""" f = tmp_path / "desktop_ipc.json" - f.write_text(json.dumps({"host": "127.0.0.1"})) + f.write_text(json.dumps({"protocol_version": SUPPORTED_PROTOCOL_VERSION})) + assert DesktopEndpoint.load(f) is None + + def test_load_without_protocol_version_returns_none(self, tmp_path): + """An unversioned endpoint cannot define the command contract.""" + f = tmp_path / "desktop_ipc.json" + f.write_text(json.dumps({"port": 51234, "token": "sess-abc"})) + assert DesktopEndpoint.load(f) is None + + def test_load_with_unknown_protocol_version_returns_none(self, tmp_path): + """A future Desktop protocol must not be guessed by an older tray.""" + f = tmp_path / "desktop_ipc.json" + f.write_text( + json.dumps( + { + "protocol_version": SUPPORTED_PROTOCOL_VERSION + 1, + "port": 51234, + "token": "sess-abc", + } + ) + ) assert DesktopEndpoint.load(f) is None def test_default_discovery_path(self): @@ -127,7 +155,15 @@ def test_from_discovery_missing_returns_none(self, tmp_path): def test_from_discovery_configures_client(self, tmp_path): """from_discovery wires host/port/token onto the client.""" f = tmp_path / "desktop_ipc.json" - f.write_text(json.dumps({"port": 40000, "token": "tok"})) + f.write_text( + json.dumps( + { + "protocol_version": SUPPORTED_PROTOCOL_VERSION, + "port": 40000, + "token": "tok", + } + ) + ) client = IPCClient.from_discovery(f) assert client is not None assert client.port == 40000 @@ -136,7 +172,15 @@ def test_from_discovery_configures_client(self, tmp_path): def test_refresh_from_discovery(self, tmp_path): """refresh_from_discovery updates an existing client in place.""" f = tmp_path / "desktop_ipc.json" - f.write_text(json.dumps({"port": 12345, "token": "t1"})) + f.write_text( + json.dumps( + { + "protocol_version": SUPPORTED_PROTOCOL_VERSION, + "port": 12345, + "token": "t1", + } + ) + ) client = IPCClient() assert client.refresh_from_discovery(f) is True assert client.port == 12345 @@ -177,3 +221,36 @@ def sendall(self, data): "pause_sync", "resume_sync", ] + + def test_connect_requests_status_as_the_first_authenticated_command(self): + """A new connection starts from Desktop state, not Tray defaults.""" + sent = [] + + class FakeSock: + def settimeout(self, _timeout): + pass + + def connect(self, _endpoint): + pass + + def recv(self, _size): + return b"" + + def sendall(self, data): + sent.append(json.loads(data.decode().strip())) + + def close(self): + pass + + client = IPCClient(token="session-token") + with patch("openadapt_tray.ipc.socket.socket", return_value=FakeSock()): + assert client.connect() is True + client.close() + + assert sent == [ + { + "type": "get_status", + "data": None, + "token": "session-token", + } + ] diff --git a/uv.lock b/uv.lock index 8d1a833..cbb29f3 100644 --- a/uv.lock +++ b/uv.lock @@ -321,51 +321,51 @@ wheels = [ [[package]] name = "cryptography" -version = "49.0.0" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -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" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, ] [[package]]