diff --git a/docs/source/releasenotes.rst b/docs/source/releasenotes.rst index eed8d23899..fddf7a3f9b 100644 --- a/docs/source/releasenotes.rst +++ b/docs/source/releasenotes.rst @@ -12,6 +12,10 @@ Latest versions `Upcoming release `_ +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +- ``RPA.Windows``: New ``Get Checkbox State`` keyword to retrieve the toggle state of a checkbox control, returning ``True`` (checked), ``False`` (unchecked) or ``None`` (indeterminate). Closes :issue:`1166`. +- ``RPA.Windows``: Fix ``path:`` locator strategy ignoring the ``timeout`` parameter — path traversal now retries each step until the active timeout expires instead of failing immediately. Closes :issue:`1125`. +- ``rpaframework-core``: Fix locator parser not handling single-quoted names with spaces (e.g. ``name:'My Window'``) — both single and double quotes are now accepted as value delimiters. +- ``rpaframework-core``: Fix ``path:`` locator ignoring the active timeout — each path step now retries until the deadline expires instead of failing immediately when a child is not yet present. `Released `_ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/packages/core/src/RPA/core/windows/locators.py b/packages/core/src/RPA/core/windows/locators.py index cb11d1edbf..db55db24c2 100644 --- a/packages/core/src/RPA/core/windows/locators.py +++ b/packages/core/src/RPA/core/windows/locators.py @@ -186,8 +186,8 @@ class MatchObject: } TREE_SEP = " > " PATH_SEP = "|" # path locator index separator - QUOTE = '"' # enclosing quote character - _LOCATOR_REGEX = re.compile(rf"\S*{QUOTE}[^{QUOTE}]+{QUOTE}|\S+", re.IGNORECASE) + QUOTE = '"' # enclosing quote character (double-quote; single-quote also accepted) + _LOCATOR_REGEX = re.compile(r"""\S*"[^"]+"|\S*'[^']+'|\S+""", re.IGNORECASE) _LOGGER = logging.getLogger(__name__) locators: List[Tuple] = field(default_factory=list) @@ -245,7 +245,7 @@ def handle_locator_part( default_values.append(part_text) def add_locator(self, control_strategy: str, value: str, level: int = 0) -> None: - value = value.strip(f"{self.QUOTE} ") + value = value.strip("\"' ") if not value: return @@ -410,7 +410,7 @@ def _get_control_from_path( Returns: Control at the end of the path Raises: - ElementNotFound: If path cannot be followed + ElementNotFound: If path cannot be followed within the active timeout """ # Follow a path in the tree of controls until reaching the final target. search_params = search_params.copy() # to keep idempotent behaviour @@ -422,42 +422,63 @@ def _get_control_from_path( max_retries = 3 retry_delay = 0.1 + # Respect the active timeout (set via @with_timeout / SetGlobalSearchTimeout). + # self.current_timeout reads auto.uiautomation.TIME_OUT_SECOND which is kept + # in sync by the set_timeout() context manager. + timeout = self.current_timeout if IS_WINDOWS else 0.0 + deadline = time.monotonic() + timeout for index, position in enumerate(path): - # Retry logic for GetChildren() which can raise COM errors - last_error = None - children = None - for attempt in range(max_retries): - try: - children = current.GetChildren() - break - except COMError as err: - last_error = err - if LocatorMethods._is_com_cantcallout_error(err) and attempt < max_retries - 1: - self.logger.debug( - "COM error 0x8001010d during GetChildren (attempt %d/%d), retrying...", - attempt + 1, - max_retries, - ) - time.sleep(retry_delay) - continue - # Non-retryable error or last attempt, raise immediately - raise - if children is None and last_error is not None: - # If we exhausted retries, re-raise the last error - raise last_error - - if position > len(children): - raise ElementNotFound( - f"Unable to retrieve child on position {position!r} under a parent" - f" with partial path {to_path(index)!r}" + # Retry at this path step until the child appears or the timeout expires. + while True: + # Inner retry loop for transient COM errors on GetChildren(). + last_error = None + children = None + for attempt in range(max_retries): + try: + children = current.GetChildren() + break + except COMError as err: + last_error = err + if ( + LocatorMethods._is_com_cantcallout_error(err) + and attempt < max_retries - 1 + ): + self.logger.debug( + "COM error 0x8001010d during GetChildren" + " (attempt %d/%d), retrying...", + attempt + 1, + max_retries, + ) + time.sleep(retry_delay) + continue + # Non-retryable error or last attempt, raise immediately + raise + if children is None and last_error is not None: + raise last_error + + if position <= len(children): + break # Child is available — proceed + + # Child not present yet; wait if the timeout allows it. + remaining = deadline - time.monotonic() + if remaining <= 0: + raise ElementNotFound( + f"Unable to retrieve child on position {position!r} under a" + f" parent with partial path {to_path(index)!r}" + ) + self.logger.debug( + "Child at position %d not yet available (%d children found)," + " retrying for %.2fs...", + position, + len(children), + remaining, ) + time.sleep(min(retry_delay, remaining)) current = children[position - 1] # Log position only to avoid deadlock from control.__repr__ - self.logger.debug( - "On child position %d found control", position - ) + self.logger.debug("On child position %d found control", position) offset = search_params.get("offset") current.robocorp_click_offset = offset diff --git a/packages/core/tests/python/test_windows.py b/packages/core/tests/python/test_windows.py index c04ecb1790..7d0120432b 100644 --- a/packages/core/tests/python/test_windows.py +++ b/packages/core/tests/python/test_windows.py @@ -50,11 +50,11 @@ class TestMatchObject: ("Name", "classx:Classx Test2", 0), ], ), - ("'Robocorp Window'", [("Name", "'Robocorp Window'", 0)]), + ("'Robocorp Window'", [("Name", "Robocorp Window", 0)]), ( "name:'Robocorp Window'", - [("Name", "'Robocorp", 0), ("Name", "Window'", 0)], - ), # single quotes don't work for enclosing, use double + [("Name", "Robocorp Window", 0)], + ), # single quotes work as delimiters, same as double quotes ('Robocorp" Window', [("Name", 'Robocorp" Window', 0)]), ( 'name:Robocorp" Window class:"My Class"', diff --git a/packages/windows/pyproject.toml b/packages/windows/pyproject.toml index ed57861698..073fc5f107 100644 --- a/packages/windows/pyproject.toml +++ b/packages/windows/pyproject.toml @@ -44,6 +44,7 @@ repository = "https://github.com/robocorp/rpaframework" [dependency-groups] dev = [ "rpaframework-devutils", + "wxPython>=4.2.0; sys_platform == 'win32'", ] [tool.uv] diff --git a/packages/windows/resources/testapp.py b/packages/windows/resources/testapp.py new file mode 100644 index 0000000000..b4bd02f2c6 --- /dev/null +++ b/packages/windows/resources/testapp.py @@ -0,0 +1,374 @@ +"""RPA.Windows test application. + +A self-contained tkinter GUI that exposes every control type exercised by +the RPA.Windows test suite: + + - Buttons (normal, disabled) + - Checkboxes (checked / unchecked / indeterminate tri-state) + - Radio buttons + - Text entry / multi-line text + - Combobox (dropdown) + - Listbox + - Slider (Scale) + - Spinner (Spinbox) + - Labels (static and dynamic status) + - Notebook (tabs) — useful for path: locator tests + - Treeview table + +Usage +----- +Run directly:: + + python testapp.py + +Or from Robot Framework test setup:: + + Start Process python ${RESOURCES}/testapp.py alias=testapp + # give it time to appear + Sleep 1s + +The window title is "RPA Windows Test App" and the app stays open until +the "Quit" button or the window close button is pressed. +""" + +import tkinter as tk +from tkinter import ttk + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _section(parent: tk.Widget, title: str) -> ttk.LabelFrame: + frame = ttk.LabelFrame(parent, text=title, padding=8) + return frame + + +# --------------------------------------------------------------------------- +# Tab builders +# --------------------------------------------------------------------------- + +def _build_tab_basic(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Buttons, checkboxes, radio buttons.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Basic Controls") + + # --- Buttons ------------------------------------------------------------ + btn_frame = _section(tab, "Buttons") + btn_frame.pack(fill="x", pady=4) + + ttk.Button( + btn_frame, + text="Click Me", + name="btn_click_me", + command=lambda: status_var.set("Button 'Click Me' pressed"), + ).grid(row=0, column=0, padx=4, pady=4) + + ttk.Button( + btn_frame, + text="Action Button", + name="btn_action", + command=lambda: status_var.set("Button 'Action Button' pressed"), + ).grid(row=0, column=1, padx=4, pady=4) + + disabled_btn = ttk.Button(btn_frame, text="Disabled Button", name="btn_disabled") + disabled_btn.state(["disabled"]) + disabled_btn.grid(row=0, column=2, padx=4, pady=4) + + # --- Checkboxes --------------------------------------------------------- + chk_frame = _section(tab, "Checkboxes") + chk_frame.pack(fill="x", pady=4) + + chk_checked_var = tk.BooleanVar(value=True) + ttk.Checkbutton( + chk_frame, + text="Checked", + name="chk_checked", + variable=chk_checked_var, + command=lambda: status_var.set(f"Checked: {chk_checked_var.get()}"), + ).grid(row=0, column=0, sticky="w", padx=4, pady=2) + + chk_unchecked_var = tk.BooleanVar(value=False) + ttk.Checkbutton( + chk_frame, + text="Unchecked", + name="chk_unchecked", + variable=chk_unchecked_var, + command=lambda: status_var.set(f"Unchecked: {chk_unchecked_var.get()}"), + ).grid(row=1, column=0, sticky="w", padx=4, pady=2) + + # Tri-state / indeterminate checkbox — tkinter represents this with a + # StringVar where "" means indeterminate, "1" = on, "0" = off. + chk_tri_var = tk.StringVar(value="") + chk_tri = ttk.Checkbutton( + chk_frame, + text="Indeterminate (tri-state)", + name="chk_indeterminate", + variable=chk_tri_var, + onvalue="1", + offvalue="0", + command=lambda: status_var.set(f"Tri-state value: '{chk_tri_var.get()}'"), + ) + chk_tri.state(["alternate"]) # start in indeterminate visual state + chk_tri.grid(row=2, column=0, sticky="w", padx=4, pady=2) + + # --- Radio buttons ------------------------------------------------------ + radio_frame = _section(tab, "Radio Buttons") + radio_frame.pack(fill="x", pady=4) + + radio_var = tk.StringVar(value="Option A") + for col, opt in enumerate(("Option A", "Option B", "Option C")): + ttk.Radiobutton( + radio_frame, + text=opt, + name=f"radio_{opt.replace(' ', '_').lower()}", + variable=radio_var, + value=opt, + command=lambda v=opt: status_var.set(f"Radio selected: {v}"), + ).grid(row=0, column=col, padx=8) + + +def _build_tab_input(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Entry, combobox, slider, spinbox.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Input Controls") + + # --- Text entry --------------------------------------------------------- + entry_frame = _section(tab, "Text Entry") + entry_frame.pack(fill="x", pady=4) + + ttk.Label(entry_frame, text="Single-line:").grid(row=0, column=0, sticky="w", padx=4) + entry_single = ttk.Entry(entry_frame, name="entry_single", width=30) + entry_single.insert(0, "Hello RPA") + entry_single.grid(row=0, column=1, padx=4, pady=2) + + ttk.Label(entry_frame, text="Password:").grid(row=1, column=0, sticky="w", padx=4) + entry_pwd = ttk.Entry(entry_frame, name="entry_password", show="*", width=30) + entry_pwd.grid(row=1, column=1, padx=4, pady=2) + + ttk.Label(entry_frame, text="Multi-line:").grid(row=2, column=0, sticky="nw", padx=4) + text_multi = tk.Text(entry_frame, name="text_multiline", width=30, height=4) + text_multi.insert("1.0", "Line 1\nLine 2\n") + text_multi.grid(row=2, column=1, padx=4, pady=2) + + # --- Combobox ----------------------------------------------------------- + combo_frame = _section(tab, "Combobox") + combo_frame.pack(fill="x", pady=4) + + ttk.Label(combo_frame, text="Select fruit:").grid(row=0, column=0, sticky="w", padx=4) + combo = ttk.Combobox( + combo_frame, + name="combo_fruit", + values=["Apple", "Banana", "Cherry", "Date", "Elderberry"], + state="readonly", + width=20, + ) + combo.set("Apple") + combo.grid(row=0, column=1, padx=4, pady=4) + combo.bind( + "<>", + lambda e: status_var.set(f"Combo selected: {combo.get()}"), + ) + + # --- Slider ------------------------------------------------------------- + slider_frame = _section(tab, "Slider") + slider_frame.pack(fill="x", pady=4) + + slider_var = tk.IntVar(value=50) + ttk.Label(slider_frame, text="Volume:").grid(row=0, column=0, sticky="w", padx=4) + slider = ttk.Scale( + slider_frame, + name="slider_volume", + from_=0, + to=100, + orient="horizontal", + variable=slider_var, + length=200, + command=lambda v: status_var.set(f"Slider: {int(float(v))}"), + ) + slider.grid(row=0, column=1, padx=4, pady=4) + slider_label = ttk.Label(slider_frame, textvariable=slider_var, width=4) + slider_label.grid(row=0, column=2, padx=4) + + # --- Spinbox ------------------------------------------------------------ + spin_frame = _section(tab, "Spinbox") + spin_frame.pack(fill="x", pady=4) + + ttk.Label(spin_frame, text="Quantity:").grid(row=0, column=0, sticky="w", padx=4) + spin = ttk.Spinbox( + spin_frame, + name="spin_quantity", + from_=0, + to=99, + width=8, + command=lambda: status_var.set(f"Spinbox: {spin.get()}"), + ) + spin.set(5) + spin.grid(row=0, column=1, padx=4, pady=4) + + +def _build_tab_list(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Listbox and treeview table.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="List & Table") + + # --- Listbox ------------------------------------------------------------ + list_frame = _section(tab, "Listbox") + list_frame.pack(fill="x", pady=4) + + listbox = tk.Listbox(list_frame, name="listbox_items", height=5, selectmode="single") + for item in ("Alpha", "Beta", "Gamma", "Delta", "Epsilon"): + listbox.insert(tk.END, item) + listbox.select_set(0) + listbox.pack(side="left", fill="both", expand=True, padx=4, pady=4) + listbox.bind( + "<>", + lambda e: status_var.set( + f"Listbox: {listbox.get(listbox.curselection()[0])}" + if listbox.curselection() + else "" + ), + ) + + sb = ttk.Scrollbar(list_frame, orient="vertical", command=listbox.yview) + listbox.configure(yscrollcommand=sb.set) + sb.pack(side="right", fill="y") + + # --- Treeview table ----------------------------------------------------- + tree_frame = _section(tab, "Table (Treeview)") + tree_frame.pack(fill="both", expand=True, pady=4) + + columns = ("name", "department", "salary") + tree = ttk.Treeview( + tree_frame, + name="tree_employees", + columns=columns, + show="headings", + height=5, + ) + for col, heading in zip(columns, ("Name", "Department", "Salary")): + tree.heading(col, text=heading) + tree.column(col, width=120) + + rows = [ + ("Alice", "Engineering", "90000"), + ("Bob", "Marketing", "75000"), + ("Carol", "Engineering", "95000"), + ("Dave", "HR", "65000"), + ("Eve", "Engineering", "88000"), + ] + for row in rows: + tree.insert("", tk.END, values=row) + + tree.pack(fill="both", expand=True, padx=4, pady=4) + tree.bind( + "<>", + lambda e: status_var.set( + f"Table row: {tree.item(tree.focus(), 'values')}" + ), + ) + + +def _build_tab_dynamic(nb: ttk.Notebook, status_var: tk.StringVar) -> None: + """Controls that appear/disappear on demand — useful for testing timeout.""" + tab = ttk.Frame(nb, padding=10) + nb.add(tab, text="Dynamic Controls") + + dyn_frame = _section(tab, "Delayed Element") + dyn_frame.pack(fill="x", pady=4) + + placeholder = ttk.Frame(dyn_frame, name="dynamic_placeholder", height=40) + placeholder.pack(fill="x", padx=4, pady=4) + + _hidden_label: list[ttk.Label] = [] + + def _show_label(delay_ms: int) -> None: + if _hidden_label: + _hidden_label[0].destroy() + _hidden_label.clear() + status_var.set(f"Label will appear in {delay_ms // 1000}s...") + + def _create() -> None: + lbl = ttk.Label( + placeholder, + name="dynamic_label", + text="I appeared after the delay!", + ) + lbl.pack() + _hidden_label.append(lbl) + status_var.set("Dynamic label is now visible.") + + tab.after(delay_ms, _create) + + def _hide_label() -> None: + if _hidden_label: + _hidden_label[0].destroy() + _hidden_label.clear() + status_var.set("Dynamic label hidden.") + + btn_row = ttk.Frame(dyn_frame) + btn_row.pack(padx=4, pady=4) + ttk.Button(btn_row, text="Show after 1s", + command=lambda: _show_label(1000)).pack(side="left", padx=4) + ttk.Button(btn_row, text="Show after 3s", + command=lambda: _show_label(3000)).pack(side="left", padx=4) + ttk.Button(btn_row, text="Hide", + command=_hide_label).pack(side="left", padx=4) + + info = ttk.Label( + dyn_frame, + text=( + "Use 'Show after Xs' then locate 'name:dynamic_label' with a matching\n" + "timeout to exercise the path: timeout fix (issue #1125)." + ), + foreground="gray", + ) + info.pack(padx=4, pady=4) + + +# --------------------------------------------------------------------------- +# Main window +# --------------------------------------------------------------------------- + +def build_app() -> tk.Tk: + root = tk.Tk() + root.title("RPA Windows Test App") + root.resizable(True, True) + root.minsize(520, 440) + + # Main notebook + nb = ttk.Notebook(root, name="notebook_main") + nb.pack(fill="both", expand=True, padx=8, pady=8) + + # Status bar + status_var = tk.StringVar(value="Ready") + status_bar = ttk.Label( + root, + name="lbl_status", + textvariable=status_var, + relief="sunken", + anchor="w", + padding=(4, 2), + ) + status_bar.pack(fill="x", side="bottom") + + # Build tabs + _build_tab_basic(nb, status_var) + _build_tab_input(nb, status_var) + _build_tab_list(nb, status_var) + _build_tab_dynamic(nb, status_var) + + # Quit button + ttk.Button( + root, + text="Quit", + name="btn_quit", + command=root.destroy, + ).pack(side="bottom", pady=4) + + return root + + +if __name__ == "__main__": + app = build_app() + app.mainloop() diff --git a/packages/windows/resources/testapp_wx.py b/packages/windows/resources/testapp_wx.py new file mode 100644 index 0000000000..faba161fe8 --- /dev/null +++ b/packages/windows/resources/testapp_wx.py @@ -0,0 +1,165 @@ +"""wxPython test application for RPA.Windows UIAutomation testing. + +Unlike tkinter, wxPython wraps native Win32 controls which properly expose +UIAutomation accessibility attributes (control types, names, patterns). + +Controls exposed: + + - wx.CheckBox → CheckBoxControl + TogglePattern (name = label text) + - wx.RadioButton → RadioButtonControl + SelectionItemPattern (name = label text) + - wx.TextCtrl → EditControl + ValuePattern (name = label from StaticText) + - wx.ComboBox → ComboBoxControl + ValuePattern (name = label from StaticText) + - wx.Button → ButtonControl + InvokePattern (name = label text) + - wx.StaticText → TextControl (name = label text) + +Window title: "RPA Windows Test App WX" + +Usage:: + + python testapp_wx.py +""" + +import wx + + +class TestFrame(wx.Frame): + def __init__(self): + super().__init__(None, title="RPA Windows Test App WX", size=(420, 560)) + panel = wx.Panel(self) + vbox = wx.BoxSizer(wx.VERTICAL) + + # ------------------------------------------------------------------ # + # Checkboxes + # ------------------------------------------------------------------ # + chk_box = wx.StaticBox(panel, label="Checkboxes") + chk_sizer = wx.StaticBoxSizer(chk_box, wx.VERTICAL) + + self.chk_checked = wx.CheckBox(panel, label="Checked") + self.chk_checked.SetValue(True) + + self.chk_unchecked = wx.CheckBox(panel, label="Unchecked") + self.chk_unchecked.SetValue(False) + + # wx.CHK_3STATE enables the indeterminate (middle) state. + # wx.CHK_ALLOW_3RD_STATE_FOR_USER lets the user cycle into it. + self.chk_indeterminate = wx.CheckBox( + panel, + label="Indeterminate", + style=wx.CHK_3STATE | wx.CHK_ALLOW_3RD_STATE_FOR_USER, + ) + self.chk_indeterminate.Set3StateValue(wx.CHK_UNDETERMINED) + + chk_sizer.Add(self.chk_checked, 0, wx.ALL, 6) + chk_sizer.Add(self.chk_unchecked, 0, wx.ALL, 6) + chk_sizer.Add(self.chk_indeterminate, 0, wx.ALL, 6) + vbox.Add(chk_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Radio Buttons + # ------------------------------------------------------------------ # + rad_box = wx.StaticBox(panel, label="Radio Buttons") + rad_sizer = wx.StaticBoxSizer(rad_box, wx.VERTICAL) + + # wx.RB_GROUP starts a new mutually-exclusive group. + # "Option A" is selected by default (first in group). + self.rad_a = wx.RadioButton(panel, label="Option A", style=wx.RB_GROUP) + self.rad_b = wx.RadioButton(panel, label="Option B") + self.rad_a.SetValue(True) + + rad_sizer.Add(self.rad_a, 0, wx.ALL, 6) + rad_sizer.Add(self.rad_b, 0, wx.ALL, 6) + vbox.Add(rad_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Text Entry + # ------------------------------------------------------------------ # + txt_box = wx.StaticBox(panel, label="Text Entry") + txt_sizer = wx.StaticBoxSizer(txt_box, wx.VERTICAL) + + # wx.TextCtrl → EditControl + ValuePattern in UIAutomation. + self.txt_entry = wx.TextCtrl(panel, value="") + txt_sizer.Add(self.txt_entry, 0, wx.EXPAND | wx.ALL, 6) + vbox.Add(txt_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Combobox + # ------------------------------------------------------------------ # + cmb_box = wx.StaticBox(panel, label="Combobox") + cmb_sizer = wx.StaticBoxSizer(cmb_box, wx.VERTICAL) + + # wx.ComboBox → ComboBoxControl + ValuePattern in UIAutomation. + self.cmb = wx.ComboBox( + panel, + value="Apple", + choices=["Apple", "Banana", "Cherry"], + style=wx.CB_READONLY, + ) + cmb_sizer.Add(self.cmb, 0, wx.EXPAND | wx.ALL, 6) + vbox.Add(cmb_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Dynamic element (for path-locator timeout testing) + # ------------------------------------------------------------------ # + dyn_box = wx.StaticBox(panel, label="Dynamic Controls") + dyn_sizer = wx.StaticBoxSizer(dyn_box, wx.VERTICAL) + + # Placeholder panel — the dynamic label is added/removed inside it. + self._dyn_inner = wx.Panel(panel) + self._dyn_inner_sizer = wx.BoxSizer(wx.VERTICAL) + self._dyn_inner.SetSizer(self._dyn_inner_sizer) + self._dynamic_label: wx.StaticText | None = None + + btn_row = wx.BoxSizer(wx.HORIZONTAL) + btn_show = wx.Button(panel, label="Show after 3s") + btn_show.Bind(wx.EVT_BUTTON, lambda _: self._schedule_label(3000)) + btn_hide = wx.Button(panel, label="Hide") + btn_hide.Bind(wx.EVT_BUTTON, lambda _: self._hide_label()) + btn_row.Add(btn_show, 0, wx.RIGHT, 6) + btn_row.Add(btn_hide, 0) + + dyn_sizer.Add(self._dyn_inner, 0, wx.EXPAND | wx.ALL, 4) + dyn_sizer.Add(btn_row, 0, wx.ALL, 4) + vbox.Add(dyn_sizer, 0, wx.EXPAND | wx.ALL, 10) + + # ------------------------------------------------------------------ # + # Quit + # ------------------------------------------------------------------ # + btn_quit = wx.Button(panel, label="Quit") + btn_quit.Bind(wx.EVT_BUTTON, lambda _: self.Close()) + vbox.Add(btn_quit, 0, wx.ALL | wx.ALIGN_CENTER, 10) + + panel.SetSizer(vbox) + self.Centre() + self.Show() + + # ---------------------------------------------------------------------- # + # Dynamic label helpers + # ---------------------------------------------------------------------- # + + def _schedule_label(self, delay_ms: int) -> None: + self._hide_label() + wx.CallLater(delay_ms, self._show_label) + + def _show_label(self) -> None: + if self._dynamic_label: + return + # wx.StaticText is skipped by UIAutomation (non-interactive Win32 Static). + # Use a disabled Button instead — ButtonControl is always in the a11y tree. + self._dynamic_label = wx.Button(self._dyn_inner, label="DynamicContent") + self._dynamic_label.Disable() + self._dyn_inner_sizer.Add(self._dynamic_label, 0, wx.ALL, 4) + self._dyn_inner.Layout() + self.Layout() + + def _hide_label(self) -> None: + if self._dynamic_label: + self._dynamic_label.Destroy() + self._dynamic_label = None + self._dyn_inner.Layout() + self.Layout() + + +if __name__ == "__main__": + app = wx.App(False) + TestFrame() + app.MainLoop() diff --git a/packages/windows/src/RPA/Windows/keywords/action.py b/packages/windows/src/RPA/Windows/keywords/action.py index beaad8651a..2e96b941ff 100644 --- a/packages/windows/src/RPA/Windows/keywords/action.py +++ b/packages/windows/src/RPA/Windows/keywords/action.py @@ -290,6 +290,57 @@ def is_selected(self, locator: Locator) -> Optional[bool]: f"Element found with {locator!r} doesn't support selection item retrieval" ) + @keyword(tags=["action"]) + def get_checkbox_state(self, locator: Locator) -> Optional[bool]: + """Get the toggle state of a checkbox element defined by the provided `locator`. + + Returns ``True`` if the checkbox is checked, ``False`` if unchecked, and + ``None`` if the state is indeterminate. + + The ``ActionNotPossible`` exception is raised if the identified element doesn't + support the toggle pattern (i.e. is not a checkbox or similar toggleable control). + + :param locator: String locator or element object. + :returns: ``True`` if checked, ``False`` if unchecked, ``None`` if indeterminate. + + **Example: Robot Framework** + + .. code-block:: robotframework + + ${checked} = Get Checkbox State type:CheckBoxControl name:Accept + IF ${checked} + Log Checkbox is checked + END + + **Example: Python** + + .. code-block:: python + + from RPA.Windows import Windows + + lib_win = Windows() + state = lib_win.get_checkbox_state("type:CheckBoxControl name:Accept") + print(state) # True, False, or None + """ + element = self.ctx.get_element(locator) + get_toggle_pattern = getattr(element.item, "GetTogglePattern", None) + + if get_toggle_pattern: + toggle_pattern = get_toggle_pattern() + if toggle_pattern is None: + raise ActionNotPossible( + f"Element found with {locator!r} doesn't support the toggle pattern" + ) + toggle_state = toggle_pattern.ToggleState + if toggle_state == auto.ToggleState.On: + return True + if toggle_state == auto.ToggleState.Off: + return False + return None # Indeterminate + raise ActionNotPossible( + f"Element found with {locator!r} doesn't support the toggle pattern" + ) + @keyword(tags=["action", "keyboard"]) def send_keys( self, diff --git a/packages/windows/tests/robot/keywords.robot b/packages/windows/tests/robot/keywords.robot new file mode 100644 index 0000000000..1359e6c44a --- /dev/null +++ b/packages/windows/tests/robot/keywords.robot @@ -0,0 +1,149 @@ + +*** Settings *** +Library Collections +Library Process +Library RPA.Windows +Library String + +Test Setup Set Wait Time 0.7 + +Default Tags windows skip + +*** Variables *** +${RESOURCES} ${CURDIR}${/}..${/}..${/}resources +${RESULTS} ${CURDIR}${/}..${/}results + +${EXE_UIDEMO} UIDemo.exe +${EXE_CALCULATOR} calc.exe +${EXE_SPOTIFY} Spotify.exe +${LOC_TESTAPP} name:"RPA Windows Test App" +${LOC_TESTAPP_WX} name:"RPA Windows Test App WX" + +${LOC_NOTEPAD} name:Notepad class:Notepad +${LOC_CALCULATOR} subname:Calc control:WindowControl +${LOC_WORDPAD} name:"Document - WordPad" and type:WindowControl + +${TIMEOUT} 1 + +*** Keywords *** +Calculator Teardown + Send Keys keys={Esc} + Sleep 1s + Send Keys keys={Alt}1 + Sleep 1s + +Calculator Temperature Converter View + Click id:TogglePaneButton + Send Keys keys={PAGEDOWN} wait_time=0.1 # to see the Temperature view + ${temp} = Set Variable id:Temperature # works on Windows 10 and lower + ${ver} = Get OS Version + IF "${ver}" == "11" + ${temp} = Set Variable name:"Temperature Converter" + END + Click ${temp} + +Calculator Standard View + Click id:TogglePaneButton + Send Keys keys={PAGEUP} wait_time=0.1 # to see the Standard view + ${std} = Set Variable id:Standard + ${ver} = Get OS Version + IF "${ver}" == "11" + ${std} = Set Variable name:"Standard Calculator" + END + Click ${std} + +Select Temperature Unit + [Arguments] ${unit} ${degrees} + # Select Unit + Click id:Units1 + Click id:Units1 > name:${unit} + Send Keys keys=${degrees} + Log To Console Set ${unit}: ${degrees} + Send Keys keys={Esc} + +Get Temperature Values + [Arguments] ${keys} + ${values} = Split String ${keys} , + FOR ${val} IN @{values} + ${temperature} = Get Attribute id:UnitConverterRootGrid > type:TextControl and regex:.*\\s${val} Name + Log To Console ${temperature} + END + +Calculator button actions + Control Window Calculator type:Window + Click id:num9Button + Click id:num6Button + Click id:plusButton + Click id:num4Button + Click id:equalButton + ${result} = Get Attribute id:CalculatorResults Name + Log To Console \n${result} + Send Keys keys={Esc} + +Calculator with keys + Control Window Calculator + Click id:clearButton + Send Keys keys=96+4= interval=0.1 + ${result} = Get Attribute id:CalculatorResults Name + Log To Console ${result} + ${buttons} = Get Elements type:Group and name:"Number pad" > type:Button + FOR ${button} IN @{buttons} + Log To Console ${button} + END + Length Should Be ${buttons} 11 msg=From 0 to 9 and decimlar separator + +Keep open a single Notepad + Set Global Timeout ${TIMEOUT} + ${closed} = Set Variable 0 + ${run} = Run Keyword And Ignore Error Close Window ${LOC_NOTEPAD} control:WindowControl + IF "${run}[0]" == "PASS" + ${closed} = Set Variable ${run}[1] + END + Log Closed Notepads: ${closed} + Windows Run Notepad + +Kill app by name + [Arguments] ${app_name} + ${window_list} = List Windows + FOR ${win} IN @{window_list} + ${exists} = Evaluate re.match(".*${app_name}.*", """${win}[title]""") + IF ${exists} + ${command} = Set Variable os.kill($win["pid"], signal.SIGTERM) + Log Killing app: ${win}[title] (PID: $win["pid"]) + Evaluate ${command} signal + END + END + +Close Current Window And Sleep + Close Current Window + Sleep 1s + +# --------------------------------------------------------------------------- +# Test App — testapp.py +# --------------------------------------------------------------------------- + +Start Test App + [Documentation] Launch the local testapp.py and return its window element. + Start Process python ${RESOURCES}${/}testapp.py alias=testapp + Sleep 1.5s + ${win} = Control Window ${LOC_TESTAPP} + RETURN ${win} + +Stop Test App + Terminate Process handle=testapp + Wait For Process handle=testapp timeout=5s on_timeout=kill + + +# --------------------------------------------------------------------------- +# WX Test App — testapp_wx.py (wxPython: proper UIAutomation exposure) +# --------------------------------------------------------------------------- + +Start Test App WX + [Documentation] Launch testapp_wx.py and set it as the active window. + Start Process uv run python ${RESOURCES}${/}testapp_wx.py alias=testapp_wx + Sleep 2s + Control Window ${LOC_TESTAPP_WX} + +Stop Test App WX + Terminate Process handle=testapp_wx + Wait For Process handle=testapp_wx timeout=5s on_timeout=kill \ No newline at end of file diff --git a/packages/windows/tests/robot/test_windows.robot b/packages/windows/tests/robot/test_windows.robot index 35ed9e8641..144c9feb2f 100644 --- a/packages/windows/tests/robot/test_windows.robot +++ b/packages/windows/tests/robot/test_windows.robot @@ -1,124 +1,20 @@ *** Settings *** -Library Collections -Library Process -Library RPA.Windows -Library String +Resource keywords.robot -Test Setup Set Wait Time 0.7 -Default Tags windows skip - - -*** Variables *** -${RESOURCES} ${CURDIR}${/}..${/}resources -${RESULTS} ${CURDIR}${/}..${/}results - -${EXE_UIDEMO} UIDemo.exe -${EXE_CALCULATOR} calc.exe -${EXE_SPOTIFY} Spotify.exe - -${LOC_NOTEPAD} name:Notepad class:Notepad -${LOC_CALCULATOR} subname:Calc control:WindowControl -${LOC_WORDPAD} name:"Document - WordPad" and type:WindowControl - -${TIMEOUT} 1 - - -*** Keywords *** -Calculator Teardown - Send Keys keys={Esc} - Sleep 1s - Send Keys keys={Alt}1 - Sleep 1s - -Calculator Temperature Converter View - Click id:TogglePaneButton - Send Keys keys={PAGEDOWN} wait_time=0.1 # to see the Temperature view - ${temp} = Set Variable id:Temperature # works on Windows 10 and lower - ${ver} = Get OS Version - IF "${ver}" == "11" - ${temp} = Set Variable name:"Temperature Converter" - END - Click ${temp} - -Calculator Standard View - Click id:TogglePaneButton - Send Keys keys={PAGEUP} wait_time=0.1 # to see the Standard view - ${std} = Set Variable id:Standard - ${ver} = Get OS Version - IF "${ver}" == "11" - ${std} = Set Variable name:"Standard Calculator" - END - Click ${std} - -Select Temperature Unit - [Arguments] ${unit} ${degrees} - # Select Unit - Click id:Units1 - Click id:Units1 > name:${unit} - Send Keys keys=${degrees} - Log To Console Set ${unit}: ${degrees} - Send Keys keys={Esc} - -Get Temperature Values - [Arguments] ${keys} - ${values} = Split String ${keys} , - FOR ${val} IN @{values} - ${temperature} = Get Attribute id:UnitConverterRootGrid > type:TextControl and regex:.*\\s${val} Name - Log To Console ${temperature} - END - -Calculator button actions - Control Window Calculator type:Window - Click id:num9Button - Click id:num6Button - Click id:plusButton - Click id:num4Button - Click id:equalButton - ${result} = Get Attribute id:CalculatorResults Name - Log To Console \n${result} - Send Keys keys={Esc} - -Calculator with keys - Control Window Calculator - Click id:clearButton - Send Keys keys=96+4= interval=0.1 - ${result} = Get Attribute id:CalculatorResults Name - Log To Console ${result} - ${buttons} = Get Elements type:Group and name:"Number pad" > type:Button - FOR ${button} IN @{buttons} - Log To Console ${button} - END - Length Should Be ${buttons} 11 msg=From 0 to 9 and decimlar separator - -Keep open a single Notepad - Set Global Timeout ${TIMEOUT} - ${closed} = Set Variable 0 - ${run} = Run Keyword And Ignore Error Close Window ${LOC_NOTEPAD} control:WindowControl - IF "${run}[0]" == "PASS" - ${closed} = Set Variable ${run}[1] - END - Log Closed Notepads: ${closed} - Windows Run Notepad - -Kill app by name - [Arguments] ${app_name} - ${window_list} = List Windows - FOR ${win} IN @{window_list} - ${exists} = Evaluate re.match(".*${app_name}.*", """${win}[title]""") - IF ${exists} - ${command} = Set Variable os.kill($win["pid"], signal.SIGTERM) - Log Killing app: ${win}[title] (PID: $win["pid"]) - Evaluate ${command} signal - END - END - -Close Current Window And Sleep - Close Current Window - Sleep 1s *** Test Cases *** +Test App Inspect Tree + [Documentation] Print the UIAutomation tree of the test app for locator discovery. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + Print Tree log_as_warnings=${True} + + [Teardown] Stop Test App + Windows search Calculator by clicking buttons Windows Search Calculator wait_time=1 Calculator button actions @@ -484,3 +380,226 @@ Click and set values in WordPad Should Be Equal ${text} This i\rs my test text.\r\r\r2nd line text.\r\r [Teardown] Close Current Window + +*** Test Cases *** + + +Test App Inspect Tree + [Documentation] Print the UIAutomation tree of the test app for locator discovery. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + Print Tree log_as_warnings=${True} + + [Teardown] Stop Test App + + +Test App Checkboxes + [Documentation] Verify Get Checkbox State returns correct values for + ... checked, unchecked and indeterminate checkboxes (issue #1166). + ... NOTE: tkinter does not expose UIAutomation attributes — run + ... Test App WX Checkboxes for the automated version of this test. + [Tags] manual + [Setup] Start Test App + + # Navigate to the "Basic Controls" tab (it is the first tab, already active). + Control Window ${LOC_TESTAPP} + + # Checked checkbox — should return True + ${state} = Get Checkbox State name:Checked + Should Be True ${state} msg=Expected 'Checked' checkbox to be True + + # Unchecked checkbox — should return False + ${state} = Get Checkbox State name:Unchecked + Should Be True ${state} == ${False} msg=Expected 'Unchecked' checkbox to be False + + # Indeterminate checkbox — should return None + ${state} = Get Checkbox State name:"Indeterminate (tri-state)" + Should Be Equal ${state} ${None} msg=Expected indeterminate checkbox to be None + + [Teardown] Stop Test App + +Test App Radio Buttons + [Documentation] Verify Is Selected works on radio buttons inside the test app. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # "Option A" starts selected + ${selected} = Is Selected name:"Option A" + Should Be True ${selected} + + # "Option B" starts unselected + ${selected} = Is Selected name:"Option B" + Should Be True ${selected} == ${False} + + [Teardown] Stop Test App + +Test App Text Entry + [Documentation] Set and get values in single-line text entry. + ... NOTE: tkinter does not expose UIAutomation attributes — manual only. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # The LabelFrame "Text Entry" is a Group with Name="Text Entry". + # The first EditControl inside it is the single-line entry. + ${entry} = Get Element + ... name:"Text Entry" > type:EditControl + Set Value ${entry} New text value + ${val} = Get Value ${entry} + Should Contain ${val} New text value + + [Teardown] Stop Test App + +Test App Combobox + [Documentation] Select a value from the combobox and confirm via Get Value. + ... NOTE: tkinter does not expose UIAutomation attributes — manual only. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + # Switch to the "Input Controls" tab + Click name:"Input Controls" + + # Combobox UIAutomation Name = currently selected value ("Apple" on start). + Select name:Apple Banana + # After selection the combobox Name changes to "Banana" — locate and confirm. + ${elem} = Get Element name:Banana + ${val} = Get Value ${elem} + Should Be Equal ${val} Banana + + [Teardown] Stop Test App + +Test App Slider + [Documentation] Move the slider and verify its value changes. + ... The ttk.Scale widget is a SliderControl in UIAutomation. + ... The exact locator depends on how Windows exposes the widget name; + ... update the locator after inspecting with Accessibility Insights. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + Click name:"Input Controls" + + # Locate the slider within the "Slider" LabelFrame (Group) by control type. + ${slider} = Get Element + ... name:Slider > type:SliderControl + Set Value ${slider} 75 + ${val} = Get Value ${slider} + Should Be True ${val} >= 70 + + [Teardown] Stop Test App + +Test App Path Locator Timeout + [Documentation] Trigger a delayed element (3 s) and assert that a locator with + ... timeout=5 finds it while timeout=1 raises ElementNotFound + ... (exercises the path: timeout fix, issue #1125). + ... NOTE: tkinter does not expose UIAutomation attributes — run + ... Test App WX Path Locator Timeout for the automated version. + [Tags] manual + [Setup] Start Test App + + Control Window ${LOC_TESTAPP} + + Click name:"Dynamic Controls" + + # The label should NOT exist yet — a short timeout must raise quickly. + ${result} = Run Keyword And Ignore Error + ... Get Element + ... name:dynamic_label + ... timeout=0.5 + Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed + + # Now show after 3 s delay, then find it within 5 s timeout. + Click name:"Show after 3s" + ${elem} = Get Element + ... name:dynamic_label + ... timeout=5 + Should Not Be Equal ${elem} ${None} + + [Teardown] Stop Test App + + + + +Test App WX Checkboxes + [Documentation] Verify Get Checkbox State against the wxPython test app. + ... wxPython uses native Win32 controls that expose proper + ... UIAutomation TogglePattern (issue #1166). + [Setup] Start Test App WX + + # Checked checkbox — should return True + ${state} = Get Checkbox State name:Checked + Should Be True ${state} msg=Expected 'Checked' checkbox to be True + + # Unchecked checkbox — should return False + ${state} = Get Checkbox State name:Unchecked + Should Be True ${state} == ${False} msg=Expected 'Unchecked' checkbox to be False + + # Indeterminate (tri-state) checkbox — should return None + ${state} = Get Checkbox State name:Indeterminate + Should Be Equal ${state} ${None} msg=Expected indeterminate checkbox to be None + + [Teardown] Stop Test App WX + + +Test App WX Path Locator Timeout + [Documentation] Verify the path: locator timeout fix (issue #1125) using the + ... wxPython test app. "Show after 3s" creates a StaticText + ... control after a 3-second delay. A 0.5 s timeout must fail + ... fast; a 5 s timeout must succeed. + [Setup] Start Test App WX + + # Element does not exist yet — short timeout must fail quickly. + ${result} = Run Keyword And Ignore Error + ... Get Element name:DynamicContent timeout=0.5 + Should Be Equal ${result}[0] FAIL msg=Short timeout should have failed + + # Trigger the 3-second delayed element, then find it within 8 s. + Click name:"Show after 3s" + Sleep 0.5s + ${elem} = Get Element name:DynamicContent timeout=8 + Should Not Be Equal ${elem} ${None} + + [Teardown] Stop Test App WX + +Test App WX Radio Buttons + [Documentation] Verify Is Selected returns correct values for radio buttons. + ... "Option A" starts selected; "Option B" starts unselected. + [Setup] Start Test App WX + + ${sel_a} = Is Selected name:"Option A" + Should Be True ${sel_a} + + ${sel_b} = Is Selected name:"Option B" + Should Be True ${sel_b} == ${False} + + [Teardown] Stop Test App WX + +Test App WX Text Entry + [Documentation] Set and read a value from the single-line text entry (EditControl). + [Setup] Start Test App WX + + ${entry} = Get Element type:EditControl + Set Value ${entry} Hello RPA + ${val} = Get Value ${entry} + Should Be Equal ${val} Hello RPA + + [Teardown] Stop Test App WX + +Test App WX Combobox + [Documentation] Select "Banana" from the combobox and confirm via Get Value. + [Setup] Start Test App WX + + Select type:ComboBoxControl Banana + ${elem} = Get Element type:ComboBoxControl + ${val} = Get Value ${elem} + Should Be Equal ${val} Banana + + [Teardown] Stop Test App WX diff --git a/packages/windows/uv.lock b/packages/windows/uv.lock index 3c890ad498..d1a94f1025 100644 --- a/packages/windows/uv.lock +++ b/packages/windows/uv.lock @@ -1199,6 +1199,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2134,6 +2206,8 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "rpaframework-devutils" }, + { name = "wxpython", version = "4.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "wxpython", version = "4.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, ] [package.metadata] @@ -2151,7 +2225,10 @@ requires-dist = [ ] [package.metadata.requires-dev] -dev = [{ name = "rpaframework-devutils", editable = "../devutils" }] +dev = [ + { name = "rpaframework-devutils", editable = "../devutils" }, + { name = "wxpython", marker = "sys_platform == 'win32'", specifier = ">=4.2.0" }, +] [[package]] name = "ruff" @@ -2595,6 +2672,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] +[[package]] +name = "wxpython" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "typing-extensions", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/6e/b70e6dbdd7cb4f154b7ca424b4c7799f7b067f7a9f4204b8d16d6464648f/wxpython-4.2.4.tar.gz", hash = "sha256:2eb123979c87bcb329e8a2452269d60ff8f9f651e9bf25c67579e53c4ebbae3c", size = 58583054, upload-time = "2025-10-29T13:22:37.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/db/eb0c6734ebf244e31cbb041d5b9f8de98ba2c3aac2c106b583d67756d2c9/wxpython-4.2.4-cp310-cp310-win32.whl", hash = "sha256:2d020c0b0db1869d58ee646afd9cc1276520a77a9d800d407927bfee35678970", size = 14791506, upload-time = "2025-10-29T04:02:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/11/56/a29cf6bfb3f049983697945dc2c1b576b0d10612e1f53bcec69204f72df3/wxpython-4.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:95d390e0bf4bd8c4c1d77e2205887a5bc84e950e4f44719d66f8d14f59c777eb", size = 16795426, upload-time = "2025-10-29T04:02:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/2c34ec1796592f4dc393a7153131a770e117054cd07e7ba1e5594c6078b2/wxpython-4.2.4-cp311-cp311-win32.whl", hash = "sha256:b86b0258074f4ec16b234274fa0c32c42e039124c9f6f36529091c9a00ebff4d", size = 14497499, upload-time = "2025-10-29T04:02:10.604Z" }, + { url = "https://files.pythonhosted.org/packages/4a/13/20311125881142802ca3f2fc743e82696dee562f26d7e35da649c26de7cc/wxpython-4.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f48fe7b9f22c7733a06b5901559b5b7e4835fa852cbef0f620ffd0a0030aaf73", size = 16538730, upload-time = "2025-10-29T04:02:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/0c/35/b25b712097115ba734adafc26ac543840da90dc2f1b0c67257fed3d3ba63/wxpython-4.2.4-cp311-cp311-win_arm64.whl", hash = "sha256:fe83813241dfb94780f18dff1678d1ffd097116a8b8fea0272a332c387f69ef8", size = 15524815, upload-time = "2025-10-29T04:02:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1f/ddbb597c3d821d4206f85234736a4f19ef1b8875eefbf8d072ffcc01c1a4/wxpython-4.2.4-cp312-cp312-win32.whl", hash = "sha256:83e45e4d5d139638260c2f23108a94cb8d40bd5eb714d41b1009452e4ec4229a", size = 14507902, upload-time = "2025-10-29T04:02:22.309Z" }, + { url = "https://files.pythonhosted.org/packages/90/c3/dfe74d7eb046612a3e475dce8ffda70341516129a7cb17fd60c8ae143304/wxpython-4.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:c333113be1fcb4e4252890b3e66af016f03432b2fe15b370f38f2a505f5daa74", size = 16549996, upload-time = "2025-10-29T04:02:24.792Z" }, + { url = "https://files.pythonhosted.org/packages/bb/29/5286f960de8079264e7a89ca4b4beae86844520b9521a1497c4908a8cae6/wxpython-4.2.4-cp312-cp312-win_arm64.whl", hash = "sha256:88b7e8cbdb141ebb4e361cd6f3d5595160764f0c05d2d7e03506b53860eb01ab", size = 15534326, upload-time = "2025-10-29T04:02:27.421Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/57b0110944f62ce37c4e3e5f9f1a116b6df2b7b8a3406a90b95272c211ad/wxpython-4.2.4-cp313-cp313-win32.whl", hash = "sha256:b0920fe193e2bbd90c6d3fbb9a8101f1276229638aff29f66bfe521901c3a0b8", size = 14506941, upload-time = "2025-10-29T04:02:34.513Z" }, + { url = "https://files.pythonhosted.org/packages/77/d4/e1f63f1b4fe97a12f2dbe76feba678b2a35fd2d9d9c8c60ba94a271f01e8/wxpython-4.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:a0a532ee6e6a3ce0d1fb2e223159d4b8ec42f8a844c852da936fb23cba6b9335", size = 16549467, upload-time = "2025-10-29T04:02:36.666Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/df43d6fcbde2389894a068e7d41d44820a19f1bb7a78daa8fb687cd8f1a8/wxpython-4.2.4-cp313-cp313-win_arm64.whl", hash = "sha256:e82ab099df0b6c55bcfad198822648a6275bbb575ce09502a9085b290ec9852d", size = 15533173, upload-time = "2025-10-29T04:02:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/72/6e/62f3c7c2f509057888ad87d141c4c77d375cfb8e4ac820654d3b972b529f/wxpython-4.2.4-cp314-cp314-win32.whl", hash = "sha256:7e7c3baa2aab55f1c0a14fdd1b371f8ec0cff1cc2f6171497643f977b243f750", size = 14848210, upload-time = "2025-10-29T04:02:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/e0/6d6762d2857963abc06f56fc14a605d1c69d83b0abf3a0702fcffb78bf3a/wxpython-4.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:dae6d80067f64498e530d2c5f47c81f5f1a6f32e820b3b59f05062c1629ff97d", size = 16918656, upload-time = "2025-10-29T04:02:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/d437e56efe7e1ec5bb7af59d6651913e18e3fa9ecdacdec7a129d5285baa/wxpython-4.2.4-cp314-cp314-win_arm64.whl", hash = "sha256:f7b649542969c2221d2963695de11c74c12060e83d412f4017ba03e7787c9c5d", size = 15807097, upload-time = "2025-10-29T04:02:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/a9/aa/37897138e3703c5560c0fbc225787faa1b08128f46f7d5e60f8f0102025c/wxpython-4.2.4-cp39-cp39-win32.whl", hash = "sha256:e31e645ed4f0b048954f230f9a3d8dce84054fe9161f2f3ceeb2bbf176126424", size = 14789341, upload-time = "2025-10-29T04:02:58.8Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/b0bd12d10041b063fa39c8adb2e35137e0c50c97e3c742f0061fead65725/wxpython-4.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:1771079f39cfe1fd52a2302bf7d59fad716b71cb5369c9423480b1a518a72afd", size = 16795520, upload-time = "2025-10-29T04:03:01.056Z" }, +] + +[[package]] +name = "wxpython" +version = "4.2.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/43/81657a6b126ffc19163500a8184d683cec08eb4e1d06905cd0c371c702d0/wxpython-4.2.5.tar.gz", hash = "sha256:44e836d1bccd99c38790bb034b6ecf70d9060f6734320560f7c4b0d006144793", size = 58732217, upload-time = "2026-02-08T20:40:42.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/4e/e6fb441868988c8f1eff9903fc3943387699e6cb4f03f09c37f935bb955c/wxpython-4.2.5-cp310-cp310-win32.whl", hash = "sha256:d7e3b69a1f2ad383e8305efee11130dbe7542b6f6d8d5ba02fdce3cfc4c67ad6", size = 14804965, upload-time = "2026-02-08T20:39:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/6c/48/9d861888d3f9ff6d0559fe6578020d65fb5f5f8821be384a33d9331cfb4e/wxpython-4.2.5-cp310-cp310-win_amd64.whl", hash = "sha256:74fc76f8d22226e607d7072278cc09052da0cda508dea14a9933b014a4e5aeeb", size = 16823187, upload-time = "2026-02-08T20:39:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/06405cbaf7168648c913c4737b810bccb6b434f0693832cbe621d0852827/wxpython-4.2.5-cp311-cp311-win32.whl", hash = "sha256:ade4d6b39c39770146ce1010df7d7a5a767d148b478133d49c36dffb0880a174", size = 14508676, upload-time = "2026-02-08T20:39:48.17Z" }, + { url = "https://files.pythonhosted.org/packages/dd/95/5b4928819f161fb4cb38a5b5a001abb8d66500b2399bf20f914215e8e17e/wxpython-4.2.5-cp311-cp311-win_amd64.whl", hash = "sha256:508de82ac2b64aa575a543949c44dc7f83a734622bed01d8501474ab454f7bb7", size = 16566588, upload-time = "2026-02-08T20:39:51.49Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/64a16f064b188903c5ade786468bb1221879f500e0aae98acc70be9c0f7d/wxpython-4.2.5-cp311-cp311-win_arm64.whl", hash = "sha256:9bf5704eedd21bd1fe3143d8ed1cfe8250e1fbb651eb6c17a73533136745fe5a", size = 15530728, upload-time = "2026-02-08T20:39:54.533Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/5136bf39877640c8a254f1370279943366d04d321461a450fcef53722f38/wxpython-4.2.5-cp312-cp312-win32.whl", hash = "sha256:e7079d9a7374b3fd5896bdea7c73faa8da52e1fbcce5368796b5c22d7de747a6", size = 14519633, upload-time = "2026-02-08T20:40:03.577Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/2c2c9dbf78f9524daf79014337e193531332c3598b16ccc11290dad9c17f/wxpython-4.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:c54962f0524662d16591a03c786cd4d71bc43c70ede8244e0a5a59aa3979d124", size = 16577057, upload-time = "2026-02-08T20:40:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/a7ed092d0426cc98ab1a907c9e6161d8846e0de0450447b877048a8ef4e3/wxpython-4.2.5-cp312-cp312-win_arm64.whl", hash = "sha256:cda1fb351caa4555bd18717f610c9a3b03d25e64db4a22e004b141f91d02fa8c", size = 15537110, upload-time = "2026-02-08T20:40:09.022Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/a69ad0a1e7b01876619982b6cf8db0e26d0f3776b9be73b61d9f0662a2ce/wxpython-4.2.5-cp313-cp313-win32.whl", hash = "sha256:0985f190565b94635f146989886196a7e9faced8911800910460919cb72668cc", size = 14520419, upload-time = "2026-02-08T20:40:17.401Z" }, + { url = "https://files.pythonhosted.org/packages/a8/bd/d2698369dbc43aa5c9324c23fdd5cd3b23c245861e334b1d976209913f90/wxpython-4.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:3fd3649fc4752f1a02776b7057073c932e5229bbab2031762b01532bcc6bd074", size = 16576741, upload-time = "2026-02-08T20:40:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4e/4181734a2bc05940ba4feb3feb2474416b1dc12c329a2ac164632582c4d6/wxpython-4.2.5-cp313-cp313-win_arm64.whl", hash = "sha256:b794d9912464990ea1fd3744fb73fbd7446149e230e5a611ba40eb4ac74755a1", size = 15537287, upload-time = "2026-02-08T20:40:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/51/a7/261bf54686192ebefc42b494da97ba3312bd01c1d37a964f0cb83f271cf0/wxpython-4.2.5-cp314-cp314-win32.whl", hash = "sha256:230ecb4de65a8d2f8bc30bccd4d64366ac3a7cf53759b77920de927d156ad9c5", size = 14859697, upload-time = "2026-02-08T20:40:33.108Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9a/73f12041178db3728a809ce37c2b64409291cb45567b2918df478f0ceb20/wxpython-4.2.5-cp314-cp314-win_amd64.whl", hash = "sha256:eb1c228f0c20ed93f2799ebd81780abc7fd65cfa8f6b65e989b68c0c18c52707", size = 16947346, upload-time = "2026-02-08T20:40:35.583Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/3a39f5fe78a7194c848919c4b681f432fe937006e2e5182a17dd519f8c91/wxpython-4.2.5-cp314-cp314-win_arm64.whl", hash = "sha256:d4439bf4b18ac720afbcf51c37d7822ba62ab6999501e96cce1dfc2f55a19344", size = 15809574, upload-time = "2026-02-08T20:40:38.859Z" }, +] + [[package]] name = "xmltodict" version = "1.0.4"