pybenutils 7.9.3__tar.gz → 7.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {pybenutils-7.9.3/pybenutils.egg-info → pybenutils-7.11.0}/PKG-INFO +1 -2
  2. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/simple_browser_controller_cls.py +405 -6
  3. {pybenutils-7.9.3 → pybenutils-7.11.0/pybenutils.egg-info}/PKG-INFO +1 -2
  4. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils.egg-info/requires.txt +0 -1
  5. {pybenutils-7.9.3 → pybenutils-7.11.0}/pyproject.toml +1 -2
  6. {pybenutils-7.9.3 → pybenutils-7.11.0}/DEPLOYMENT.md +0 -0
  7. {pybenutils-7.9.3 → pybenutils-7.11.0}/LICENSE +0 -0
  8. {pybenutils-7.9.3 → pybenutils-7.11.0}/MANIFEST.in +0 -0
  9. {pybenutils-7.9.3 → pybenutils-7.11.0}/README.md +0 -0
  10. {pybenutils-7.9.3 → pybenutils-7.11.0}/deploy_requirements.txt +0 -0
  11. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/__init__.py +0 -0
  12. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/amazon_boto3/__init__.py +0 -0
  13. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/amazon_boto3/amazon_obj.py +0 -0
  14. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/autogui/__init__.py +0 -0
  15. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/autogui/__main__.py +0 -0
  16. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/autogui/auto_gui_cls.py +0 -0
  17. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/__init__.py +0 -0
  18. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/chrome.py +0 -0
  19. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/firefox.py +0 -0
  20. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/selenium_utils.py +0 -0
  21. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/web_driver.py +0 -0
  22. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/browsers/windows_browsers_keywords.py +0 -0
  23. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/cli_tools.py +0 -0
  24. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/__init__.py +0 -0
  25. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/download_manager.py +0 -0
  26. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/http_cert_info.py +0 -0
  27. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/s3_bucket_cls.py +0 -0
  28. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/ssh_helper.py +0 -0
  29. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/network/ssh_utils.py +0 -0
  30. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/__init__.py +0 -0
  31. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/files_and_directories.py +0 -0
  32. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/mac_application_control.py +0 -0
  33. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/mac_input_control.py +0 -0
  34. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/mac_operations.py +0 -0
  35. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/mac_popup_handler.py +0 -0
  36. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/process.py +0 -0
  37. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/os_operations/window_operations.py +0 -0
  38. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/proxmox_utils.py +0 -0
  39. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/tests_files_utils.py +0 -0
  40. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/useful.py +0 -0
  41. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/utils_logger/__init__.py +0 -0
  42. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/utils_logger/config_logger.py +0 -0
  43. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils/windows_registry.py +0 -0
  44. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils.egg-info/SOURCES.txt +0 -0
  45. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils.egg-info/dependency_links.txt +0 -0
  46. {pybenutils-7.9.3 → pybenutils-7.11.0}/pybenutils.egg-info/top_level.txt +0 -0
  47. {pybenutils-7.9.3 → pybenutils-7.11.0}/setup.cfg +0 -0
  48. {pybenutils-7.9.3 → pybenutils-7.11.0}/tests/test_self_import.py +0 -0
  49. {pybenutils-7.9.3 → pybenutils-7.11.0}/webserver/pyserver.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybenutils
3
- Version: 7.9.3
3
+ Version: 7.11.0
4
4
  Summary: PyBEN Utilities repository contains a variety of useful methods and classes designed to allow easy access to high-level operations
5
5
  Author: Ben Moskovitch
6
6
  Author-email: Ben Moskovitch <darkflameben@gmail.com>
@@ -29,7 +29,6 @@ Requires-Dist: selenium>=4.39.0
29
29
  Requires-Dist: multiprocess>=0.70.18
30
30
  Requires-Dist: boto>=2.49.0
31
31
  Requires-Dist: pysocks>=1.7.1
32
- Requires-Dist: typing>=3.7.4.3
33
32
  Requires-Dist: idna>=3.11
34
33
  Requires-Dist: pyOpenSSL>=25.3.0
35
34
  Requires-Dist: cryptography>=46.0.3
@@ -1,3 +1,6 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
1
4
  import sys
2
5
  import time
3
6
  from pathlib import Path
@@ -12,12 +15,151 @@ from pybenutils.browsers.windows_browsers_keywords import set_browser_url, close
12
15
  if sys.platform == 'win32':
13
16
  import win32com.client
14
17
  import win32gui
15
- else:
18
+ elif sys.platform == 'darwin':
16
19
  from pybenutils.os_operations.mac_operations import run_apple_script
17
20
 
18
21
  logger = get_logger()
19
22
 
20
23
 
24
+ def _linux_run(cmd, check=False, capture=True, timeout=10):
25
+ """Run shell command on Linux and return CompletedProcess.
26
+
27
+ :param cmd: list or str command
28
+ :param check: raise on non-zero exit
29
+ :param capture: capture stdout/stderr
30
+ :param timeout: seconds
31
+ """
32
+ try:
33
+ return subprocess.run(
34
+ cmd,
35
+ shell=isinstance(cmd, str),
36
+ check=check,
37
+ capture_output=capture,
38
+ text=True,
39
+ timeout=timeout,
40
+ )
41
+ except Exception as ex:
42
+ logger.error(f'_linux_run failed: cmd={cmd} err={ex}')
43
+ return None
44
+
45
+
46
+ def _xdotool(*args, timeout=10):
47
+ """Invoke xdotool with the given args. Returns stdout str (empty on failure)."""
48
+ if not shutil.which('xdotool'):
49
+ logger.error('xdotool not installed. Run: sudo apt install xdotool')
50
+ return ''
51
+ res = _linux_run(['xdotool', *map(str, args)], timeout=timeout)
52
+ if res and res.returncode == 0:
53
+ return (res.stdout or '').strip()
54
+ if res and res.stderr:
55
+ logger.debug(f'xdotool stderr: {res.stderr.strip()}')
56
+ return ''
57
+
58
+
59
+ def _xdotool_activate(window_name):
60
+ """Activate (focus + raise) a window by name match (case-insensitive)."""
61
+ wid = _xdotool('search', '--name', window_name)
62
+ if not wid:
63
+ return False
64
+ # search returns one wid per line; take last (newest)
65
+ wid = wid.splitlines()[-1].strip()
66
+ _xdotool('windowactivate', '--sync', wid)
67
+ return True
68
+
69
+
70
+ # Map of Windows SendKeys named keys to xdotool keysyms
71
+ _WIN_SENDKEYS_NAMED = {
72
+ 'F1': 'F1', 'F2': 'F2', 'F3': 'F3', 'F4': 'F4', 'F5': 'F5', 'F6': 'F6',
73
+ 'F7': 'F7', 'F8': 'F8', 'F9': 'F9', 'F10': 'F10', 'F11': 'F11', 'F12': 'F12',
74
+ 'ENTER': 'Return',
75
+ 'TAB': 'Tab',
76
+ 'ESC': 'Escape',
77
+ 'BACKSPACE': 'BackSpace', 'BS': 'BackSpace', 'BKSP': 'BackSpace',
78
+ 'DEL': 'Delete', 'DELETE': 'Delete',
79
+ 'HOME': 'Home',
80
+ 'END': 'End',
81
+ 'PGUP': 'Prior',
82
+ 'PGDN': 'Next',
83
+ 'INS': 'Insert', 'INSERT': 'Insert',
84
+ 'LEFT': 'Left', 'RIGHT': 'Right', 'UP': 'Up', 'DOWN': 'Down',
85
+ 'SPACE': 'space',
86
+ '-': 'minus', '+': 'plus',
87
+ '{': 'braceleft', '}': 'braceright',
88
+ '[': 'bracketleft', ']': 'bracketright',
89
+ '(': 'parenleft', ')': 'parenright',
90
+ '^': 'asciicircum', '%': 'percent', '~': 'asciitilde',
91
+ }
92
+
93
+
94
+ def _sendkeys_to_xdotool_chord(text):
95
+ """Parse a Windows-style SendKeys string into a list of xdotool key chords.
96
+
97
+ Returns None if `text` looks like a literal string (no SendKeys metacharacters),
98
+ so the caller can fall back to `xdotool type`.
99
+
100
+ Supported subset (sufficient for pybenutils browser keywords):
101
+ {KEYNAME} -> single named key (e.g. {F5}, {ENTER}, {TAB})
102
+ ^x -> Ctrl+x (single character)
103
+ +x -> Shift+x
104
+ %x -> Alt+x
105
+ ^+{KEY} -> Ctrl+Shift+key
106
+ ^{KEY} -> Ctrl+key
107
+ Sequences may be concatenated, e.g. "^+{K}", "^t", "{F5}"
108
+ """
109
+ # Quick reject — if no SendKeys metachar present, treat as plain text.
110
+ if not any(c in text for c in ('{', '^', '+', '%', '~')):
111
+ return None
112
+ # Heuristic: if the entire string is wrapped braces or starts with a modifier
113
+ # / brace, try to parse. Otherwise it's likely text containing a stray char.
114
+ if not (text.startswith('{') or text.startswith('^') or text.startswith('+')
115
+ or text.startswith('%') or text.startswith('~')):
116
+ return None
117
+
118
+ chords = []
119
+ i = 0
120
+ while i < len(text):
121
+ mods = []
122
+ # collect modifiers
123
+ while i < len(text) and text[i] in ('^', '+', '%'):
124
+ mods.append({'^': 'ctrl', '+': 'shift', '%': 'alt'}[text[i]])
125
+ i += 1
126
+ if i >= len(text):
127
+ break
128
+ ch = text[i]
129
+ if ch == '{':
130
+ # find closing brace
131
+ end = text.find('}', i)
132
+ if end == -1:
133
+ return None
134
+ keyname = text[i + 1:end].strip().upper()
135
+ keysym = _WIN_SENDKEYS_NAMED.get(keyname, keyname)
136
+ i = end + 1
137
+ else:
138
+ # single literal char following modifiers
139
+ keysym = ch
140
+ i += 1
141
+ chord = '+'.join(mods + [keysym]) if mods else keysym
142
+ chords.append(chord)
143
+ return chords if chords else None
144
+
145
+
146
+ # Mapping from generic browser name to Linux binary name (used to launch / pgrep).
147
+ LINUX_BROWSER_BINARY_TABLE = {
148
+ 'chrome': 'google-chrome',
149
+ 'chromium': 'chromium-browser',
150
+ 'firefox': 'firefox',
151
+ 'msedge': 'microsoft-edge',
152
+ }
153
+ # Mapping from generic browser name to the visible window title fragment used
154
+ # by xdotool to focus the window.
155
+ LINUX_BROWSER_WINDOW_NAME_TABLE = {
156
+ 'chrome': 'Google Chrome',
157
+ 'chromium': 'Chromium',
158
+ 'firefox': 'Mozilla Firefox',
159
+ 'msedge': 'Microsoft Edge',
160
+ }
161
+
162
+
21
163
  def kill_all_browsers(browsers_list=('iexplore.exe', 'IEDriverServer.exe', 'chrome', 'firefox', 'chromedriver',
22
164
  'safari', 'chromium', 'geckodriver', 'msedge'),
23
165
  attempt_graceful_shutdown=True):
@@ -79,19 +221,29 @@ class SimpleBrowserController:
79
221
  'msedge.exe': 'Chrome_WidgetWin_1'
80
222
  }
81
223
  self.browser_name = browser_name
82
- if sys.platform != 'win32' and browser_name in process_names_mac_conversion_table:
224
+ # Remember the original key (e.g. 'chrome') for Linux lookups
225
+ self._browser_key = browser_name
226
+ if sys.platform == 'darwin' and browser_name in process_names_mac_conversion_table:
83
227
  self.browser_name = process_names_mac_conversion_table[browser_name]
84
228
  self.mac_browser_path = mac_application_path_conversion_table.get(browser_name, '')
85
229
 
230
+ # Linux-specific lookups
231
+ self.linux_binary = LINUX_BROWSER_BINARY_TABLE.get(browser_name, browser_name)
232
+ self.linux_window_name = LINUX_BROWSER_WINDOW_NAME_TABLE.get(browser_name, browser_name)
233
+
86
234
  self.browser_process_name = self.browser_name
87
235
  if self.browser_name in ['chrome', 'chrome.exe', 'chromium', 'chromium.exe']:
88
236
  self.browser_process_name = 'chrome.exe'
89
237
  elif sys.platform == 'win32' and not self.browser_process_name.endswith('.exe'):
90
238
  self.browser_process_name = '{brw}.exe'.format(brw=self.browser_process_name)
239
+ # On Linux the process name is the binary name (no .exe), e.g. 'chrome' / 'firefox'.
240
+ if sys.platform == 'linux':
241
+ # Default to the short browser key so ProcessHandler matches "chrome" etc.
242
+ self.browser_process_name = self._browser_key
91
243
 
92
244
  self.hwnd = []
93
245
  self.app_obj = None
94
- if sys.platform != 'win32':
246
+ if sys.platform == 'darwin':
95
247
  from pybenutils.os_operations.mac_application_control import ApplicationControl
96
248
  self.app_obj = ApplicationControl(self.mac_browser_path)
97
249
  else:
@@ -110,6 +262,90 @@ class SimpleBrowserController:
110
262
  return False
111
263
  self.hwnd.append(new_handler)
112
264
  return True
265
+ elif sys.platform == 'linux':
266
+ binary = self.linux_binary if shutil.which(self.linux_binary) else self._browser_key
267
+ if not shutil.which(binary):
268
+ logger.error(f'Browser binary not found in PATH: {binary}')
269
+ return False
270
+ # Build chrome-specific args:
271
+ # - --ozone-platform=x11 forces X11 backend (else chrome attaches to
272
+ # the user's Wayland session and renders on the real desktop :0)
273
+ # - --user-data-dir=<tmp> isolates from the user's existing chrome
274
+ # profile (chrome's singleton would otherwise forward our URL to
275
+ # the running chrome on :0 instead of opening a window on our :100)
276
+ # - --no-first-run / --no-default-browser-check suppress modal dialogs
277
+ chrome_force_x11 = []
278
+ if 'chrome' in binary or 'chromium' in binary or 'edge' in binary:
279
+ # Use a fixed persistent profile dir so site permissions
280
+ # (granted manually once) survive across test runs.
281
+ # If env var PYBENUTILS_CHROME_PROFILE is set, use that;
282
+ # otherwise default to ~/gpa_chrome_profile.
283
+ persistent_profile = os.environ.get(
284
+ 'PYBENUTILS_CHROME_PROFILE',
285
+ os.path.expanduser('~/gpa_chrome_profile')
286
+ )
287
+ os.makedirs(persistent_profile, exist_ok=True)
288
+ chrome_force_x11 = [
289
+ '--ozone-platform=x11',
290
+ '--no-sandbox',
291
+ '--disable-gpu',
292
+ '--disable-dev-shm-usage',
293
+ f'--user-data-dir={persistent_profile}',
294
+ '--no-first-run',
295
+ '--no-default-browser-check',
296
+ '--new-window',
297
+ '--window-size=1280,1024',
298
+ # Auto-launch eaaclient:// protocol from the Akamai IDP origin
299
+ # without prompting. Matches the Chrome enterprise policy
300
+ # AutoLaunchProtocolsFromOrigins but as a cmdline override.
301
+ '--auto-launch-protocols-from-origins='
302
+ 'eaaclient,https://etp-client.login.go.akamai-access.com',
303
+ # Bypass the "request blocked" page by treating the IDP origin
304
+ # as a secure origin and disabling popup blocking for it.
305
+ '--disable-popup-blocking',
306
+ # Suppress "Chrome didn't shut down correctly" Restore popup
307
+ # that blocks the IDP page after profile clone.
308
+ '--disable-session-crashed-bubble',
309
+ '--disable-features=InfiniteSessionRestore',
310
+ '--hide-crash-restore-bubble',
311
+ # NOTE: --incognito tried earlier but broke first-auth
312
+ # because incognito doesn't load the profile permissions
313
+ # (Allow popup gets re-asked). The cloned profile's
314
+ # cookies/permissions are required.
315
+ ]
316
+ cmd = [binary, *chrome_force_x11, *map(str, arguments)] if arguments else \
317
+ [binary, *chrome_force_x11]
318
+ # Strip Wayland env so chrome falls back to X11 (DISPLAY only).
319
+ child_env = os.environ.copy()
320
+ for var in ('WAYLAND_DISPLAY', 'XDG_SESSION_TYPE', 'GDK_BACKEND',
321
+ 'QT_QPA_PLATFORM', 'CLUTTER_BACKEND', 'MOZ_ENABLE_WAYLAND'):
322
+ child_env.pop(var, None)
323
+ # Ensure DISPLAY is set; xvfb-run exports it.
324
+ if 'DISPLAY' not in child_env:
325
+ logger.warning('DISPLAY env not set; xdotool focus will fail. Run under xvfb-run.')
326
+ logger.info(f'Linux launch: {" ".join(cmd)} (DISPLAY={child_env.get("DISPLAY","<unset>")})')
327
+ try:
328
+ # Detach so the test can continue while browser runs
329
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
330
+ stdin=subprocess.DEVNULL, start_new_session=True,
331
+ env=child_env)
332
+ except Exception as ex:
333
+ logger.error(f'Failed to launch {binary}: {ex}')
334
+ return False
335
+ # Give chrome time to actually map its window in xvfb
336
+ for attempt in range(20):
337
+ time.sleep(1)
338
+ wid = _xdotool('search', '--name', self.linux_window_name)
339
+ if wid:
340
+ logger.info(f'Linux chrome window appeared on DISPLAY '
341
+ f'{child_env.get("DISPLAY","?")} after {attempt+1}s '
342
+ f'(wid={wid.splitlines()[-1]})')
343
+ return True
344
+ # Last-chance diagnostic — list all windows so we know what xdotool can see
345
+ all_wins = _xdotool('search', '--onlyvisible', '--name', '.+')
346
+ logger.warning(f'Linux chrome window NOT found after 20s. '
347
+ f'xdotool sees these visible windows: {all_wins!r}')
348
+ return False
113
349
  else:
114
350
  return self.app_obj.launch(arguments=arguments)
115
351
 
@@ -130,6 +366,12 @@ class SimpleBrowserController:
130
366
  time.sleep(20)
131
367
  self.kill(attempt_graceful_shutdown=attempt_graceful_shutdown)
132
368
  self.hwnd = []
369
+ elif sys.platform == 'linux':
370
+ # pkill matches by process name. -f matches full command line which is
371
+ # safer for snap chrome wrappers etc.
372
+ _linux_run(['pkill', '-f', self.linux_binary], timeout=5)
373
+ time.sleep(1)
374
+ _linux_run(['pkill', '-9', '-f', self.linux_binary], timeout=5)
133
375
  else:
134
376
  self.app_obj.kill()
135
377
  time.sleep(10)
@@ -152,6 +394,11 @@ class SimpleBrowserController:
152
394
  return True
153
395
  logger.warning('{n} could not be gracefully closed'.format(n=self.browser_name))
154
396
  return False
397
+ elif sys.platform == 'linux':
398
+ # Try graceful SIGTERM first; pkill returns 0 if any process matched.
399
+ _linux_run(['pkill', '-TERM', '-f', self.linux_binary], timeout=5)
400
+ time.sleep(3)
401
+ return True
155
402
  else:
156
403
  self.app_obj.close()
157
404
  time.sleep(3)
@@ -165,6 +412,47 @@ class SimpleBrowserController:
165
412
  """Returns the hwnd list parameter"""
166
413
  return self.hwnd
167
414
 
415
+ def _linux_focus_browser(self) -> bool:
416
+ """Bring the browser window to focus on Linux using xdotool. Returns True if found."""
417
+ found = _xdotool_activate(self.linux_window_name)
418
+ if not found:
419
+ logger.warning(f'_linux_focus_browser: no window matching "{self.linux_window_name}". '
420
+ f'xdotool search returned empty. Browser may not be visible to X server.')
421
+ return found
422
+
423
+ def press_key_on_linux_browser(self, keysym) -> bool:
424
+ """Send an xdotool 'key' chord (e.g. 'ctrl+l', 'Return', 'F6') to the focused browser window.
425
+
426
+ :param keysym: xdotool keysym or chord
427
+ :return: True on success
428
+ """
429
+ try:
430
+ self._linux_focus_browser()
431
+ logger.debug(f'xdotool key: {keysym}')
432
+ _xdotool('key', '--clearmodifiers', keysym)
433
+ return shutil.which('xdotool') is not None
434
+ except Exception as ex:
435
+ logger.error(f'press_key_on_linux_browser failed key={keysym} err={ex}')
436
+ return False
437
+
438
+ def type_text_on_linux_browser(self, text, delay_ms=25) -> bool:
439
+ """Type literal text into the focused browser window using xdotool type.
440
+
441
+ :param text: text to type
442
+ :param delay_ms: per-char delay
443
+ :return: True on success
444
+ """
445
+ try:
446
+ self._linux_focus_browser()
447
+ # Redact obvious secrets from logs (passwords are commonly typed via this path)
448
+ preview = text if len(text) <= 60 else f'{text[:60]}...({len(text)} chars)'
449
+ logger.debug(f'xdotool type: "{preview}"')
450
+ _xdotool('type', '--clearmodifiers', '--delay', str(delay_ms), text)
451
+ return shutil.which('xdotool') is not None
452
+ except Exception as ex:
453
+ logger.error(f'type_text_on_linux_browser failed: {ex}')
454
+ return False
455
+
168
456
  def press_key_combination_on_mac_browser(self, key_command) -> bool:
169
457
  """Attempts to press the requested command on the keyboard using Applescript
170
458
 
@@ -174,6 +462,38 @@ class SimpleBrowserController:
174
462
  if sys.platform == 'win32':
175
463
  # logger.warning('You are trying to use a mac function on a windows os')
176
464
  return False
465
+ elif sys.platform == 'linux':
466
+ # Translate common Mac key codes to xdotool keysyms. Best-effort —
467
+ # anything unknown logs and returns False so callers don't crash.
468
+ mapping = {
469
+ 'key code 96': 'F5', # reload
470
+ 'key code 97 using option down': 'F6', # focus url bar
471
+ 'key code 76': 'Return',
472
+ 'key code 53 using option down': 'Escape',
473
+ 'key code 48': 'Tab',
474
+ 'key code 48 using control down': 'ctrl+Tab',
475
+ 'key code 17 using command down': 'ctrl+t', # new tab
476
+ 'key code 82 using command down': 'ctrl+0', # zoom reset
477
+ 'key code 78 using command down': 'ctrl+minus',
478
+ 'key code 69 using command down': 'ctrl+plus',
479
+ 'key code 121': 'Next',
480
+ 'key code 116': 'Prior',
481
+ 'key code 115 using command down': 'ctrl+Home',
482
+ 'key code 119 using command down': 'ctrl+End',
483
+ 'key code 0 using command down': 'ctrl+a',
484
+ 'key code 37 using command down': 'ctrl+l',
485
+ 'keystroke "c" using command down': 'ctrl+c',
486
+ 'key code 4 using {command down, shift down}': 'ctrl+shift+h',
487
+ 'key code 115 using option down': 'alt+Home',
488
+ }
489
+ key = mapping.get(key_command)
490
+ if key:
491
+ return self.press_key_on_linux_browser(key)
492
+ # Handle keystroke "literal text" pattern
493
+ if key_command.startswith('keystroke "') and key_command.endswith('"'):
494
+ return self.type_text_on_linux_browser(key_command[11:-1])
495
+ logger.debug(f'press_key_combination_on_mac_browser: no Linux mapping for "{key_command}"')
496
+ return False
177
497
  else:
178
498
  cmd = f"""
179
499
  set chExist to false
@@ -217,6 +537,9 @@ class SimpleBrowserController:
217
537
  errors.append(str(ex))
218
538
  else:
219
539
  logger.error('Failed to click on HOME button for errors: {err}'.format(err=errors))
540
+ elif sys.platform == 'linux':
541
+ # Most browsers on Linux use Alt+Home for home page
542
+ return self.press_key_on_linux_browser('alt+Home')
220
543
  else:
221
544
  if 'firefox' in self.browser_name.lower():
222
545
  return self.press_key_combination_on_mac_browser('key code 115 using option down')
@@ -236,6 +559,8 @@ class SimpleBrowserController:
236
559
  if sys.platform == 'win32':
237
560
  shell = win32com.client.Dispatch("WScript.Shell")
238
561
  shell.SendKeys("{ESC}", 1)
562
+ elif sys.platform == 'linux':
563
+ return self.press_key_on_linux_browser('Escape')
239
564
  else:
240
565
  return self.press_key_combination_on_mac_browser('key code 53 using option down')
241
566
  except Exception as ex:
@@ -252,6 +577,8 @@ class SimpleBrowserController:
252
577
  if sys.platform == 'win32':
253
578
  shell = win32com.client.Dispatch("WScript.Shell")
254
579
  shell.SendKeys("{ENTER}", 1)
580
+ elif sys.platform == 'linux':
581
+ return self.press_key_on_linux_browser('Return')
255
582
  else:
256
583
  return self.press_key_combination_on_mac_browser('key code 76')
257
584
  except Exception as ex:
@@ -268,6 +595,19 @@ class SimpleBrowserController:
268
595
  if sys.platform == 'win32':
269
596
  shell = win32com.client.Dispatch("WScript.Shell")
270
597
  shell.SendKeys(text, 1)
598
+ elif sys.platform == 'linux':
599
+ # The Mac/Win callers send strings in two distinct shapes:
600
+ # 1. Literal text to type: "etp.automation@gmail.com"
601
+ # 2. Windows SendKeys notation: "{F5}", "^a", "^+{K}", "^t" etc.
602
+ # xdotool's `type` is literal-only; `key` takes a chord. Translate
603
+ # SendKeys-style sequences to xdotool key chords. Plain text falls
604
+ # through to xdotool type.
605
+ converted = _sendkeys_to_xdotool_chord(text)
606
+ if converted is not None:
607
+ for chord in converted:
608
+ self.press_key_on_linux_browser(chord)
609
+ else:
610
+ self.type_text_on_linux_browser(text)
271
611
  else:
272
612
  self.press_key_combination_on_mac_browser('keystroke "{text}"'.format(text=text))
273
613
  except Exception as ex:
@@ -285,6 +625,9 @@ class SimpleBrowserController:
285
625
  if sys.platform == 'win32':
286
626
  shell = win32com.client.Dispatch("WScript.Shell")
287
627
  shell.SendKeys("{F6}", 1)
628
+ elif sys.platform == 'linux':
629
+ # Ctrl+L focuses the address bar in Chrome/Firefox on Linux (F6 also works)
630
+ self.press_key_on_linux_browser('ctrl+l')
288
631
  else:
289
632
  self.press_key_combination_on_mac_browser('key code 97 using option down')
290
633
  except Exception as ex:
@@ -295,6 +638,8 @@ class SimpleBrowserController:
295
638
  if sys.platform == 'win32':
296
639
  shell = win32com.client.Dispatch("WScript.Shell")
297
640
  shell.SendKeys("^c", 1)
641
+ elif sys.platform == 'linux':
642
+ self.press_key_on_linux_browser('ctrl+c')
298
643
  else:
299
644
  self.press_key_combination_on_mac_browser('keystroke "c" using command down')
300
645
  except Exception as ex:
@@ -327,6 +672,12 @@ class SimpleBrowserController:
327
672
  else:
328
673
  logger.error('Failed to set browser url for errors: {err}'.format(err=errors))
329
674
 
675
+ elif sys.platform == 'linux':
676
+ self.press_key_on_linux_browser('ctrl+l')
677
+ time.sleep(0.3)
678
+ self.type_text_on_linux_browser(url)
679
+ time.sleep(0.3)
680
+ self.press_enter_button()
330
681
  else:
331
682
  self.press_key_combination_on_mac_browser('key code 37 using command down')
332
683
  self.send_keyboard_keys(url)
@@ -345,6 +696,8 @@ class SimpleBrowserController:
345
696
  if sys.platform == 'win32':
346
697
  shell = win32com.client.Dispatch("WScript.Shell")
347
698
  shell.SendKeys("^{TAB}", 1)
699
+ elif sys.platform == 'linux':
700
+ return self.press_key_on_linux_browser('ctrl+Tab')
348
701
  else:
349
702
  return self.press_key_combination_on_mac_browser('key code 48 using control down')
350
703
  except Exception as ex:
@@ -370,6 +723,9 @@ class SimpleBrowserController:
370
723
  except Exception as ex:
371
724
  print(ex)
372
725
  return False
726
+ elif sys.platform == 'linux':
727
+ # xdotool windowactivate is the closest analogue: brings window to front + focus.
728
+ return self._linux_focus_browser()
373
729
  else:
374
730
  print('Not yet implemented')
375
731
  return False
@@ -401,6 +757,18 @@ class SimpleBrowserController:
401
757
  self.send_keyboard_keys('{F12}')
402
758
  else:
403
759
  raise AssertionError(f'The requested operation is not yet implemented for {self.browser_name}')
760
+ elif sys.platform == 'linux':
761
+ # Linux: open DevTools console, type command, Enter, close DevTools
762
+ if 'firefox' in self.browser_name.lower():
763
+ self.press_key_on_linux_browser('ctrl+shift+k')
764
+ else:
765
+ self.press_key_on_linux_browser('ctrl+shift+j')
766
+ time.sleep(1)
767
+ self.type_text_on_linux_browser(command)
768
+ time.sleep(0.5)
769
+ self.press_enter_button()
770
+ time.sleep(0.5)
771
+ self.press_key_on_linux_browser('F12')
404
772
  else:
405
773
  raise AssertionError(f'The requested operation is only implemented for windows')
406
774
 
@@ -409,14 +777,27 @@ class SimpleBrowserController:
409
777
  if sys.platform == 'win32':
410
778
  self.refresh_hwnd_list()
411
779
  return bool(self.get_hwnd())
780
+ elif sys.platform == 'linux':
781
+ # Check via xdotool (window present in the current DISPLAY's X server),
782
+ # not pgrep — pgrep would match chrome on a different DISPLAY (e.g. the
783
+ # user's :0 session while we run under xvfb on :100), giving a false
784
+ # positive that makes the caller skip launching our own chrome.
785
+ if not shutil.which('xdotool'):
786
+ # Fallback to pgrep if xdotool missing
787
+ res = _linux_run(['pgrep', '-f', self.linux_binary], timeout=5)
788
+ return bool(res and res.returncode == 0 and (res.stdout or '').strip())
789
+ return bool(_xdotool('search', '--name', self.linux_window_name))
412
790
  else:
413
791
  return self.app_obj.is_running()
414
792
 
415
793
  def press_tab_button(self):
416
794
  """Using the keyboard, press the TAB button"""
417
- result = self.send_keyboard_keys("{TAB}") if sys.platform == 'win32' else \
418
- self.press_key_combination_on_mac_browser('key code 48')
419
- return result
795
+ if sys.platform == 'win32':
796
+ return self.send_keyboard_keys("{TAB}")
797
+ elif sys.platform == 'linux':
798
+ return self.press_key_on_linux_browser('Tab')
799
+ else:
800
+ return self.press_key_combination_on_mac_browser('key code 48')
420
801
 
421
802
  def approve_chrome_changed_assets_alert(self, tab_presses=4):
422
803
  """Using the keyboard to click on "keep changes" at chrome's "accept asset change" popup
@@ -435,6 +816,8 @@ class SimpleBrowserController:
435
816
  logger.debug('Attempting to open a new tab')
436
817
  if sys.platform == 'win32':
437
818
  self.send_keyboard_keys('^t')
819
+ elif sys.platform == 'linux':
820
+ self.press_key_on_linux_browser('ctrl+t')
438
821
  else:
439
822
  self.press_key_combination_on_mac_browser('key code 17 using command down')
440
823
 
@@ -442,6 +825,8 @@ class SimpleBrowserController:
442
825
  """Press the combination ctrl/command + '0' keyboard keys - reset zoom level to 100%"""
443
826
  if sys.platform == 'win32':
444
827
  self.send_keyboard_keys('^0')
828
+ elif sys.platform == 'linux':
829
+ self.press_key_on_linux_browser('ctrl+0')
445
830
  else:
446
831
  self.press_key_combination_on_mac_browser('key code 82 using command down')
447
832
 
@@ -449,6 +834,8 @@ class SimpleBrowserController:
449
834
  """Press the combination ctrl/command + '-' keyboard keys - zoom out"""
450
835
  if sys.platform == 'win32':
451
836
  self.send_keyboard_keys('^{-}')
837
+ elif sys.platform == 'linux':
838
+ self.press_key_on_linux_browser('ctrl+minus')
452
839
  else:
453
840
  self.press_key_combination_on_mac_browser('key code 78 using command down')
454
841
 
@@ -456,6 +843,8 @@ class SimpleBrowserController:
456
843
  """Press the combination ctrl/command + '+' keyboard keys - zoom in"""
457
844
  if sys.platform == 'win32':
458
845
  self.send_keyboard_keys('^{+}')
846
+ elif sys.platform == 'linux':
847
+ self.press_key_on_linux_browser('ctrl+plus')
459
848
  else:
460
849
  self.press_key_combination_on_mac_browser('key code 69 using command down')
461
850
 
@@ -475,6 +864,8 @@ class SimpleBrowserController:
475
864
  """Press the page down key"""
476
865
  if sys.platform == 'win32':
477
866
  self.send_keyboard_keys('{PGDN}')
867
+ elif sys.platform == 'linux':
868
+ self.press_key_on_linux_browser('Next')
478
869
  else:
479
870
  self.press_key_combination_on_mac_browser('key code 121')
480
871
 
@@ -482,6 +873,8 @@ class SimpleBrowserController:
482
873
  """Press the page up key"""
483
874
  if sys.platform == 'win32':
484
875
  self.send_keyboard_keys('{PGUP}')
876
+ elif sys.platform == 'linux':
877
+ self.press_key_on_linux_browser('Prior')
485
878
  else:
486
879
  self.press_key_combination_on_mac_browser('key code 116')
487
880
 
@@ -489,6 +882,8 @@ class SimpleBrowserController:
489
882
  """Press the combination ctrl/command + home keyboard keys - Scroll to top of the page"""
490
883
  if sys.platform == 'win32':
491
884
  self.send_keyboard_keys('^{HOME}')
885
+ elif sys.platform == 'linux':
886
+ self.press_key_on_linux_browser('ctrl+Home')
492
887
  else:
493
888
  self.press_key_combination_on_mac_browser('key code 115 using command down')
494
889
 
@@ -496,6 +891,8 @@ class SimpleBrowserController:
496
891
  """Press the combination ctrl/command + end keyboard keys - Scroll to end of the page"""
497
892
  if sys.platform == 'win32':
498
893
  self.send_keyboard_keys('^{END}')
894
+ elif sys.platform == 'linux':
895
+ self.press_key_on_linux_browser('ctrl+End')
499
896
  else:
500
897
  self.press_key_combination_on_mac_browser('key code 119 using command down')
501
898
 
@@ -505,5 +902,7 @@ class SimpleBrowserController:
505
902
  self.send_keyboard_keys('^a')
506
903
  elif sys.platform == 'darwin':
507
904
  self.press_key_combination_on_mac_browser('key code 0 using command down')
905
+ elif sys.platform == 'linux':
906
+ self.press_key_on_linux_browser('ctrl+a')
508
907
  else:
509
908
  self.send_keyboard_keys('^a')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybenutils
3
- Version: 7.9.3
3
+ Version: 7.11.0
4
4
  Summary: PyBEN Utilities repository contains a variety of useful methods and classes designed to allow easy access to high-level operations
5
5
  Author: Ben Moskovitch
6
6
  Author-email: Ben Moskovitch <darkflameben@gmail.com>
@@ -29,7 +29,6 @@ Requires-Dist: selenium>=4.39.0
29
29
  Requires-Dist: multiprocess>=0.70.18
30
30
  Requires-Dist: boto>=2.49.0
31
31
  Requires-Dist: pysocks>=1.7.1
32
- Requires-Dist: typing>=3.7.4.3
33
32
  Requires-Dist: idna>=3.11
34
33
  Requires-Dist: pyOpenSSL>=25.3.0
35
34
  Requires-Dist: cryptography>=46.0.3
@@ -6,7 +6,6 @@ selenium>=4.39.0
6
6
  multiprocess>=0.70.18
7
7
  boto>=2.49.0
8
8
  pysocks>=1.7.1
9
- typing>=3.7.4.3
10
9
  idna>=3.11
11
10
  pyOpenSSL>=25.3.0
12
11
  cryptography>=46.0.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pybenutils"
7
- version = "7.9.3"
7
+ version = "7.11.0"
8
8
  description = "PyBEN Utilities repository contains a variety of useful methods and classes designed to allow easy access to high-level operations"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -46,7 +46,6 @@ dependencies = [
46
46
  "multiprocess>=0.70.18",
47
47
  "boto>=2.49.0",
48
48
  "pysocks>=1.7.1",
49
- "typing>=3.7.4.3",
50
49
  "idna>=3.11",
51
50
  "pyOpenSSL>=25.3.0",
52
51
  "cryptography>=46.0.3",
File without changes
File without changes
File without changes
File without changes
File without changes