From 8f2f4bd15fe7f6bc549161f7f1af262c49d8d2ae Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 10:12:32 +0200 Subject: [PATCH 01/19] tree_util unittests added --- tests/test_tree_util.py | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_tree_util.py diff --git a/tests/test_tree_util.py b/tests/test_tree_util.py new file mode 100644 index 0000000..8878443 --- /dev/null +++ b/tests/test_tree_util.py @@ -0,0 +1,77 @@ +import unittest +import os +from ITK_dev_shared_components.SAP import tree_util, sap_login, multi_session + +class test_tree_util(unittest.TestCase): + @classmethod + def setUpClass(cls): + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + session = multi_session.get_all_SAP_sessions()[0] + session.startTransaction("fmcacov") + session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" + session.findById("wnd[0]").sendVKey(0) + + + @classmethod + def tearDownClass(cls): + sap_login.kill_sap() + + + def test_get_node_key_by_text(self): + session = multi_session.get_all_SAP_sessions()[0] + tree = session.findById("wnd[0]/shellcont/shell") + + result = tree_util.get_node_key_by_text(tree, "25564617") + self.assertEqual(result, "GP0000000001") + + result = tree_util.get_node_key_by_text(tree, "556461", True) + self.assertEqual(result, "GP0000000001") + + with self.assertRaises(ValueError): + tree_util.get_node_key_by_text(tree, "Foo Bar") + + with self.assertRaises(ValueError): + tree_util.get_node_key_by_text(tree, "Foo Bar", True) + + + def test_get_item_by_text(self): + session = multi_session.get_all_SAP_sessions()[0] + tree = session.findById("wnd[0]/shellcont/shell") + + result = tree_util.get_item_by_text(tree, "25564617") + self.assertEqual(result, ("GP0000000001", "Column1")) + + result = tree_util.get_item_by_text(tree, "556461", True) + self.assertEqual(result, ("GP0000000001", "Column1")) + + with self.assertRaises(ValueError): + tree_util.get_item_by_text(tree, "Foo Bar") + + with self.assertRaises(ValueError): + tree_util.get_item_by_text(tree, "Foo Bar", True) + + + def test_check_uncheck_all_check_boxes(self): + session = multi_session.get_all_SAP_sessions()[0] + session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") + session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") + tree = session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]") + + # Test in different orders + tree_util.check_all_check_boxes(tree) + tree_util.uncheck_all_check_boxes(tree) + tree_util.uncheck_all_check_boxes(tree) + tree_util.check_all_check_boxes(tree) + tree_util.check_all_check_boxes(tree) + + session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[0]").pressButton("CANCEL") + + # Test on tree without checkboxes + tree = session.findById("wnd[0]/shellcont/shell") + tree_util.check_all_check_boxes(tree) + tree_util.uncheck_all_check_boxes(tree) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 562f9bd10e826dd238db6323ad7bafe7f684c3be Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 12:26:59 +0200 Subject: [PATCH 02/19] Test added/changed. Automatic unittests added --- tests/__init__.py | 0 tests/run_tests.bat | 15 +++++++++++++++ tests/test_gridview_util.py | 2 ++ tests/test_sap_login.py | 30 ++++++++++++++++++++++++++++++ tests/test_tree_util.py | 9 ++++----- 5 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/run_tests.bat create mode 100644 tests/test_sap_login.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/run_tests.bat b/tests/run_tests.bat new file mode 100644 index 0000000..902f4cb --- /dev/null +++ b/tests/run_tests.bat @@ -0,0 +1,15 @@ +:: Change dir to parent dir +cd /d %~dp0.. + +:: Delete old venv +rmdir /s /q venv + +:: Setup venv +python -m venv venv +call venv/Scripts/activate +pip install . + +:: Run all unittests +python -m unittest discover + +pause \ No newline at end of file diff --git a/tests/test_gridview_util.py b/tests/test_gridview_util.py index e1bd259..c0c3a99 100644 --- a/tests/test_gridview_util.py +++ b/tests/test_gridview_util.py @@ -5,6 +5,8 @@ class test_gridview_util(unittest.TestCase): @classmethod def setUpClass(cls): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') sap_login.login_using_cli(user, password) session = multi_session.get_all_SAP_sessions()[0] diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py new file mode 100644 index 0000000..ea2fddd --- /dev/null +++ b/tests/test_sap_login.py @@ -0,0 +1,30 @@ +import unittest +import os +from ITK_dev_shared_components.SAP import sap_login + +class test_gridview_util(unittest.TestCase): + @classmethod + def setUpClass(cls): + sap_login.kill_sap() + + @classmethod + def tearDownClass(cls): + sap_login.kill_sap() + + def setUp(self) -> None: + pass + + def test_login_with_cli(self): + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + + sap_login.kill_sap() + + with self.assertRaises(TimeoutError): + sap_login.login_using_cli(user, password, timeout=0) + + with self.assertRaises(ValueError): + sap_login.login_using_cli("Foo", "Bar") + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_tree_util.py b/tests/test_tree_util.py index 8878443..9225fe3 100644 --- a/tests/test_tree_util.py +++ b/tests/test_tree_util.py @@ -5,6 +5,8 @@ class test_tree_util(unittest.TestCase): @classmethod def setUpClass(cls): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') sap_login.login_using_cli(user, password) session = multi_session.get_all_SAP_sessions()[0] @@ -56,14 +58,11 @@ def test_check_uncheck_all_check_boxes(self): session = multi_session.get_all_SAP_sessions()[0] session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") + + # Test on tree with checkboxes tree = session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]") - - # Test in different orders tree_util.check_all_check_boxes(tree) tree_util.uncheck_all_check_boxes(tree) - tree_util.uncheck_all_check_boxes(tree) - tree_util.check_all_check_boxes(tree) - tree_util.check_all_check_boxes(tree) session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[0]").pressButton("CANCEL") From 6cfe6d52f10397fc7d0e61f4662fd82fa4fad2b8 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 13:02:18 +0200 Subject: [PATCH 03/19] docstring updated --- .../SAP/multi_session.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ITK_dev_shared_components/SAP/multi_session.py b/ITK_dev_shared_components/SAP/multi_session.py index f1ce075..75484ba 100644 --- a/ITK_dev_shared_components/SAP/multi_session.py +++ b/ITK_dev_shared_components/SAP/multi_session.py @@ -62,14 +62,23 @@ def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): batch = args[b:b+num_sessions] run_batch(func, args, len(batch)) -def spawn_sessions(num_sessions=6) -> list: +def spawn_sessions(num_sessions=6) -> tuple: """A function to spawn multiple sessions of SAP. This function will attempt to spawn the desired number of sessions. - If the current number of open sessions exceeds the desired number of sessions + If the current number of already open sessions exceeds the desired number of sessions the already open sessions will not be closed to match the desired number. The number of sessions must be between 1 and 6. - Returns a list of all open sessions. - """ + + Args: + num_sessions: The number of sessions desired. Defaults to 6. + + Raises: + ValueError: If the number of sessions is not between 1 and 6. + + Returns: + tuple: A tuple of all currently open sessions. + """ + SAP = win32com.client.GetObject("SAPGUI") app = SAP.GetScriptingEngine connection = app.Connections(0) @@ -87,7 +96,7 @@ def spawn_sessions(num_sessions=6) -> list: while connection.Sessions.count < num_sessions: time.sleep(0.1) - sessions = list(connection.Sessions) + sessions = tuple(connection.Sessions) num_sessions = len(sessions) if num_sessions == 1: From 047559efa03da31a215fca52ccf6937776ab9a83 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 13:02:58 +0200 Subject: [PATCH 04/19] typo fix --- tests/test_sap_login.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py index ea2fddd..acabbcf 100644 --- a/tests/test_sap_login.py +++ b/tests/test_sap_login.py @@ -2,7 +2,7 @@ import os from ITK_dev_shared_components.SAP import sap_login -class test_gridview_util(unittest.TestCase): +class test_sap_login(unittest.TestCase): @classmethod def setUpClass(cls): sap_login.kill_sap() From cb7d6aaffa5a0eefb772f93cdb09766e7a0ddbc0 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 13:03:13 +0200 Subject: [PATCH 05/19] test multi session added --- tests/test_multi_session.py | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_multi_session.py diff --git a/tests/test_multi_session.py b/tests/test_multi_session.py new file mode 100644 index 0000000..4deb632 --- /dev/null +++ b/tests/test_multi_session.py @@ -0,0 +1,57 @@ +import unittest +import os +import threading +from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt + +class test_multi_session(unittest.TestCase): + def setUp(self): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + + def tearDown(self): + sap_login.kill_sap() + + + def test_spawn_sessions(self): + with self.assertRaises(ValueError): + multi_session.spawn_sessions(0) + + with self.assertRaises(ValueError): + multi_session.spawn_sessions(7) + + sessions = multi_session.spawn_sessions(1) + self.assertEqual(len(sessions), 1) + + sessions = multi_session.spawn_sessions(6) + self.assertEqual(len(sessions), 6) + + sessions = multi_session.get_all_SAP_sessions() + self.assertEqual(len(sessions), 6) + + + def test_run_batches(self): + # This also tests: + # run_batch, run_with_session, ExThread + num_sessions = 6 + + multi_session.spawn_sessions(num_sessions) + + #Data + lock = threading.Lock() + + data = [ + ("25564617", None, 'Orientering', f"Hej {i}", lock) for i in range(12) + ] + + # Test with 12 cases + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data, num_sessions) + + # Test with 5 cases + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:5], num_sessions) + + # Test with 1 case + multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:1], num_sessions) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 77fcc202ad66259559b4705d9009ebab34c34ff5 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 13:15:56 +0200 Subject: [PATCH 06/19] Ugly if replaced by beautiful math --- ITK_dev_shared_components/SAP/multi_session.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/ITK_dev_shared_components/SAP/multi_session.py b/ITK_dev_shared_components/SAP/multi_session.py index 75484ba..ea07d40 100644 --- a/ITK_dev_shared_components/SAP/multi_session.py +++ b/ITK_dev_shared_components/SAP/multi_session.py @@ -1,9 +1,10 @@ -"""This module provides static function to handle multiple sessions of SAP. +"""This module provides static functions to handle multiple sessions of SAP. Using this module you can spawn multiple sessions and automatically execute a function in parallel on the sessions.""" import time import threading +import math from typing import Callable import pythoncom import win32com.client @@ -99,17 +100,9 @@ def spawn_sessions(num_sessions=6) -> tuple: sessions = tuple(connection.Sessions) num_sessions = len(sessions) - if num_sessions == 1: - c = 1 - elif num_sessions <= 4: - c = 2 - elif num_sessions <= 6: - c = 3 - - if num_sessions < 3: - r = 1 - else: - r = 2 + # Calculate number of columns and rows + c = math.ceil(math.sqrt(num_sessions)) + r = math.ceil(num_sessions / c) w, h = 1920//c, 1040//r From c6e08c891274864b8660d73eefbc9841d55051d1 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 29 Aug 2023 13:31:39 +0200 Subject: [PATCH 07/19] test kundekontakt added --- tests/test_opret_kundekontakt.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/test_opret_kundekontakt.py diff --git a/tests/test_opret_kundekontakt.py b/tests/test_opret_kundekontakt.py new file mode 100644 index 0000000..81dbb52 --- /dev/null +++ b/tests/test_opret_kundekontakt.py @@ -0,0 +1,34 @@ +import unittest +import os +import threading +from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt + +class test_opret_kundekontakt(unittest.TestCase): + def setUp(self): + sap_login.kill_sap() + user, password = os.environ['SAP Login'].split(';') + sap_login.login_using_cli(user, password) + + def tearDown(self): + sap_login.kill_sap() + + + def test_opret_kundekontakt(self): + fp = "25564617" + aftaler = ("2291987", "1990437", "1473781") + + session = multi_session.get_all_SAP_sessions()[0] + + # Test with 3 aftaler + opret_kundekontakt.opret_kundekontakter(session, fp, aftaler, 'Orientering', "Test 1") + + # Test with 1 aftale + opret_kundekontakt.opret_kundekontakter(session, fp, aftaler[0:1], 'Automatisk', "Test 2") + + # Test with 0 aftaler + opret_kundekontakt.opret_kundekontakter(session, fp, None, 'Returpost', "Test 3") + + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 2aa381b6db31ce7eca1222c9ae1504a1aa6589c8 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 10:28:11 +0200 Subject: [PATCH 08/19] SAP change password implemented --- ITK_dev_shared_components/SAP/sap_login.py | 116 ++++++++++++++++++--- 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index c2c98f5..7d82c2a 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -1,4 +1,5 @@ -"""This module provides a functions to open SAP GUI.""" +"""This module provides functions to handle opening and closing SAP Gui +as well as a function to change user passwords.""" import os import pathlib @@ -7,6 +8,7 @@ from selenium import webdriver from selenium.webdriver.common.by import By import pywintypes +import win32com.client from ITK_dev_shared_components.SAP import multi_session @@ -43,8 +45,7 @@ def login_using_portal(username:str, password:str): _wait_for_download() driver.quit() - - _wait_for_sap_to_open() + _wait_for_sap_session(10) def _wait_for_download(): @@ -65,14 +66,19 @@ def _wait_for_download(): raise TimeoutError(f".SAP file not found in {downloads_folder}") -def login_using_cli(username: str, password: str, client='751', system='P02') -> None: +def login_using_cli(username: str, password: str, client:str='751', system:str='P02', timeout:int=10) -> None: """Open and login to SAP with commandline expressions. Args: - username (str): AZ username - password (str): password - client (str, optional): Kommune ID (Aarhus = 751). Defaults to '751'. - system (str, optional): Environment SID (e.g. P02 = 'KMD OPUS Produktion [P02]'). Defaults to 'P02'. + username: AZ username + password: password + client: Kommune ID (Aarhus = 751). Defaults to '751'. + system: Environment SID (e.g. P02 = 'KMD OPUS Produktion [P02]'). Defaults to 'P02'. + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. + + Raises: + TimeoutError: If SAP doesn't start within timeout limit. + ValueError: If SAP is unable to log in using the given credentials. """ command_args = [ @@ -84,17 +90,21 @@ def login_using_cli(username: str, password: str, client='751', system='P02') -> ] subprocess.run(command_args, check=False) + _wait_for_sap_session(timeout) + if not _check_for_splash_screen(): + raise ValueError("Unable to log in. Please check username and password.") + - _wait_for_sap_to_open() - +def _wait_for_sap_session(timeout:int) -> None: + """Check every second if the SAP Gui scripting engine is available until timeout is reached. -def _wait_for_sap_to_open() -> None: - """Check every second for 10 seconds if the SAP Gui scripting engine is available. + Args: + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. Raises: - TimeoutError: If SAP doesn't start within 10 seconds. + TimeoutError: If SAP doesn't start within timeout limit. """ - for _ in range(10): + for _ in range(timeout): time.sleep(1) try: sessions = multi_session.get_all_SAP_sessions() @@ -103,11 +113,81 @@ def _wait_for_sap_to_open() -> None: except pywintypes.com_error: pass - raise TimeoutError("SAP didn't respond within 10 seconds.") + raise TimeoutError(f"SAP didn't respond within timeout limit: {timeout} seconds.") + +def _check_for_splash_screen() -> bool: + """Check if the splash screen image is currently present. + + Returns: + bool: True if the splash screen image is currently present. + """ + session = multi_session.get_all_SAP_sessions()[0] + image = session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[1]/shell", False) + if image: + return True + else: + return False + +def change_password(username:str, old_password:str, new_password:str, + client:str='751', + system:str='...KMD OPUS Produktion [P02]', + timeout:int=10): + """Change the password of a user in SAP Gui. Closes SAP when done. + + Args: + username: The username of the user. + old_password: The current password of the user. + new_password: The new password to change to. + client: The client number. Defaults to '751'. + system: The description string of the connection as displayed in SAP Logon. Defaults to '...KMD OPUS Produktion [P02]'. + timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. + + Raises: + TimeoutError: If the connection couldn't be established within the timeout limit. + + Returns: + True if the password has successfully been changed. + """ + + subprocess.Popen("C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") + + # Wait for SAP Logon to open + for _ in range(timeout): + time.sleep(1) + try: + SAP = win32com.client.GetObject("SAPGUI") + app = SAP.GetScriptingEngine + app.OpenConnection(system) + break + except pywintypes.com_error: + pass + else: + raise TimeoutError(f"SAP Logon didn't open within timeout limit: {timeout} seconds.") + + session = multi_session.get_all_SAP_sessions()[0] + + # Enter credentials + session.findById("wnd[0]/usr/txtRSYST-MANDT").text = client + session.findById("wnd[0]/usr/txtRSYST-BNAME").text = username + session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = old_password + session.findById("wnd[0]/tbar[1]/btn[5]").press() + session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password + session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password + + session.findById("wnd[1]/tbar[0]/btn[0]").press() + + success = _check_for_splash_screen() + + kill_sap() + + return success + + def kill_sap(): """Kills all SAP processes currently running.""" - os.system("taskkill /F /IM saplogon.exe") + os.system("taskkill /F /IM saplogon.exe > NUL 2>&1") + if __name__=="__main__": # user = "az12345" @@ -116,4 +196,8 @@ def kill_sap(): # login_using_cli(user, password) kill_sap() + username, password = os.environ['SAP Login'].split(';') + + change_password(username, password, "Hunter2") + From a2b72ec65e74e8ec7f3ecaa4330b3d43bc2236ae Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 10:44:56 +0200 Subject: [PATCH 09/19] Added catch + lint fix --- ITK_dev_shared_components/SAP/sap_login.py | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index 7d82c2a..2a29de2 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -123,15 +123,13 @@ def _check_for_splash_screen() -> bool: """ session = multi_session.get_all_SAP_sessions()[0] image = session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[1]/shell", False) - if image: - return True - else: - return False + + return (image is not None) def change_password(username:str, old_password:str, new_password:str, client:str='751', system:str='...KMD OPUS Produktion [P02]', - timeout:int=10): + timeout:int=10) -> None: """Change the password of a user in SAP Gui. Closes SAP when done. Args: @@ -144,12 +142,12 @@ def change_password(username:str, old_password:str, new_password:str, Raises: TimeoutError: If the connection couldn't be established within the timeout limit. + ValueError: If the current credentials are not valid. + ValueError: If the new password is not valid. - Returns: - True if the password has successfully been changed. """ - subprocess.Popen("C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") + subprocess.Popen(r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") # Wait for SAP Logon to open for _ in range(timeout): @@ -166,22 +164,24 @@ def change_password(username:str, old_password:str, new_password:str, session = multi_session.get_all_SAP_sessions()[0] + # Enter credentials session.findById("wnd[0]/usr/txtRSYST-MANDT").text = client session.findById("wnd[0]/usr/txtRSYST-BNAME").text = username session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = old_password session.findById("wnd[0]/tbar[1]/btn[5]").press() - session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password - session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password - - session.findById("wnd[1]/tbar[0]/btn[0]").press() + try: + session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password + session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password + session.findById("wnd[1]/tbar[0]/btn[0]").press() + except pywintypes.com_error: + raise ValueError("Login with current credentials failed.") - success = _check_for_splash_screen() + if not _check_for_splash_screen(): + raise ValueError("New password couldn't be set. Please check password requirements.") kill_sap() - - return success def kill_sap(): From d6430b062afdfd2ce63d31d2bd031fa09f6bb7d2 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 11:01:01 +0200 Subject: [PATCH 10/19] lint fix --- ITK_dev_shared_components/SAP/sap_login.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index 2a29de2..b064015 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -124,7 +124,7 @@ def _check_for_splash_screen() -> bool: session = multi_session.get_all_SAP_sessions()[0] image = session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[1]/shell", False) - return (image is not None) + return image is not None def change_password(username:str, old_password:str, new_password:str, client:str='751', @@ -147,7 +147,7 @@ def change_password(username:str, old_password:str, new_password:str, """ - subprocess.Popen(r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") + subprocess.Popen(r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") #pylint: disable=consider-using-with # Wait for SAP Logon to open for _ in range(timeout): @@ -175,8 +175,8 @@ def change_password(username:str, old_password:str, new_password:str, session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password session.findById("wnd[1]/tbar[0]/btn[0]").press() - except pywintypes.com_error: - raise ValueError("Login with current credentials failed.") + except pywintypes.com_error as exc: + raise ValueError("Login with current credentials failed.") from exc if not _check_for_splash_screen(): raise ValueError("New password couldn't be set. Please check password requirements.") From 3e94af5281058a0881cc6af2abbdc0064ff7c00d Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 12:30:43 +0200 Subject: [PATCH 11/19] Change password test added --- ITK_dev_shared_components/SAP/sap_login.py | 8 ++++--- tests/test_sap_login.py | 27 +++++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index b064015..32a82e6 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -176,9 +176,11 @@ def change_password(username:str, old_password:str, new_password:str, session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password session.findById("wnd[1]/tbar[0]/btn[0]").press() except pywintypes.com_error as exc: - raise ValueError("Login with current credentials failed.") from exc - + kill_sap() + raise ValueError("Password change was blocked. Check credentials.") from exc + if not _check_for_splash_screen(): + kill_sap() raise ValueError("New password couldn't be set. Please check password requirements.") kill_sap() @@ -198,6 +200,6 @@ def kill_sap(): username, password = os.environ['SAP Login'].split(';') - change_password(username, password, "Hunter2") + change_password(username, password, "Hunter3") diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py index acabbcf..10508dc 100644 --- a/tests/test_sap_login.py +++ b/tests/test_sap_login.py @@ -3,16 +3,11 @@ from ITK_dev_shared_components.SAP import sap_login class test_sap_login(unittest.TestCase): - @classmethod - def setUpClass(cls): - sap_login.kill_sap() - - @classmethod - def tearDownClass(cls): + def setUp(self) -> None: sap_login.kill_sap() - def setUp(self) -> None: - pass + def tearDown(self) -> None: + sap_login.kill_sap() def test_login_with_cli(self): user, password = os.environ['SAP Login'].split(';') @@ -25,6 +20,22 @@ def test_login_with_cli(self): with self.assertRaises(ValueError): sap_login.login_using_cli("Foo", "Bar") + + + @unittest.skip("Should be run manually") + def test_change_password(self): + username = "az12345" + password = "Hunter2" + new_password = "Hunter3" + + sap_login.change_password(username, password, new_password) + + with self.assertRaises(ValueError): + sap_login.change_password(username, "Foo", new_password) + + with self.assertRaises(ValueError): + sap_login.change_password(username, password, "") + if __name__ == '__main__': unittest.main() \ No newline at end of file From 8c5cabcfeb4d9bddd453216078c3b71bdf85afae Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 13:01:14 +0200 Subject: [PATCH 12/19] error check changed --- ITK_dev_shared_components/SAP/sap_login.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index 32a82e6..cf7698b 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -142,7 +142,7 @@ def change_password(username:str, old_password:str, new_password:str, Raises: TimeoutError: If the connection couldn't be established within the timeout limit. - ValueError: If the current credentials are not valid. + ValueError: If the current credentials are not valid or if the password can't be changed. ValueError: If the new password is not valid. """ @@ -171,13 +171,16 @@ def change_password(username:str, old_password:str, new_password:str, session.findById("wnd[0]/usr/pwdRSYST-BCODE").text = old_password session.findById("wnd[0]/tbar[1]/btn[5]").press() - try: - session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password - session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password - session.findById("wnd[1]/tbar[0]/btn[0]").press() - except pywintypes.com_error as exc: + # Check status bar + status_bar = session.findById("wnd[0]/sbar") + if status_bar.MessageType != 'S': kill_sap() - raise ValueError("Password change was blocked. Check credentials.") from exc + raise ValueError(f"Password change was blocked: {status_bar.Text}") + + # Enter new password + session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password + session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password + session.findById("wnd[1]/tbar[0]/btn[0]").press() if not _check_for_splash_screen(): kill_sap() From 3595b537c8c18ec5d1bead1ed97032e8a1b5d8a0 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 13:24:44 +0200 Subject: [PATCH 13/19] Bug fix --- tests/test_sap_login.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py index 10508dc..67f3574 100644 --- a/tests/test_sap_login.py +++ b/tests/test_sap_login.py @@ -18,6 +18,8 @@ def test_login_with_cli(self): with self.assertRaises(TimeoutError): sap_login.login_using_cli(user, password, timeout=0) + sap_login.kill_sap() + with self.assertRaises(ValueError): sap_login.login_using_cli("Foo", "Bar") From 9973dcc7418d2d3b512f5b524ee6f24beff42003 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 15:14:47 +0200 Subject: [PATCH 14/19] Addede option to skip reset --- tests/run_tests.bat | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/run_tests.bat b/tests/run_tests.bat index 902f4cb..7b7dd49 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -1,15 +1,28 @@ +@echo off + :: Change dir to parent dir +echo Changing directory... cd /d %~dp0.. -:: Delete old venv -rmdir /s /q venv -:: Setup venv -python -m venv venv -call venv/Scripts/activate -pip install . +choice /C YN /M "Do you want to reset venv?" +if errorlevel 2 ( + echo Activating excisting venv... + call venv\Scripts\activate + +) else ( + echo Removing old venv... + rmdir /s /q venv + + echo Setting up new venv... + python -m venv venv + call venv\Scripts\activate + + echo Installing package... + pip install . +) -:: Run all unittests +echo Running unit tests... python -m unittest discover pause \ No newline at end of file From f942e08ee67ed43feefb6d375cf5e73c74df4067 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 15:15:16 +0200 Subject: [PATCH 15/19] Removed stupid test --- tests/test_sap_login.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py index 67f3574..bb709b5 100644 --- a/tests/test_sap_login.py +++ b/tests/test_sap_login.py @@ -1,4 +1,5 @@ import unittest +import time import os from ITK_dev_shared_components.SAP import sap_login @@ -15,11 +16,6 @@ def test_login_with_cli(self): sap_login.kill_sap() - with self.assertRaises(TimeoutError): - sap_login.login_using_cli(user, password, timeout=0) - - sap_login.kill_sap() - with self.assertRaises(ValueError): sap_login.login_using_cli("Foo", "Bar") From bc7269627c53a7f85c298ecdab2d16ae688198a5 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Thu, 31 Aug 2023 15:20:47 +0200 Subject: [PATCH 16/19] Minor change --- ITK_dev_shared_components/SAP/sap_login.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index cf7698b..b30c166 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -199,10 +199,12 @@ def kill_sap(): # password = "Hunter2" # login_using_portal(user, password) # login_using_cli(user, password) + # change_password(username, password, "Hunter3") kill_sap() username, password = os.environ['SAP Login'].split(';') + login_using_cli(username, password) - change_password(username, password, "Hunter3") + From dc466fe663c77a8c4d85f2194e3637968852cfe7 Mon Sep 17 00:00:00 2001 From: ghbm-itk <123645708+ghbm-itk@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:29:36 +0200 Subject: [PATCH 17/19] Empty table bug fixed --- ITK_dev_shared_components/SAP/gridview_util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ITK_dev_shared_components/SAP/gridview_util.py b/ITK_dev_shared_components/SAP/gridview_util.py index f927675..a78df3e 100644 --- a/ITK_dev_shared_components/SAP/gridview_util.py +++ b/ITK_dev_shared_components/SAP/gridview_util.py @@ -10,7 +10,9 @@ def scroll_entire_table(grid_view, return_to_top=False) -> None: Returns: _type_: _description_ """ - + if grid_view.RowCount == 0 or grid_view.VisibleRowCount == 0: + return + for i in range(0, grid_view.RowCount, grid_view.VisibleRowCount): grid_view.FirstVisibleRow = i From 28726fd7e9eb771dfdc20f4b97656bd188c99bf3 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Tue, 10 Oct 2023 08:23:23 +0200 Subject: [PATCH 18/19] Multiple checks re-enabled --- .pylintrc | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.pylintrc b/.pylintrc index e6d1055..efc55fc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,13 +1,7 @@ [pylint.messages_control] disable = - C0303, # Trailing whitespace - C0103, # Variable names - C0305, # Trailing newlines - C0304, # Missing final line C0301, # Line too long I1101, E1101, # C-modules members - W0621, # Redefine outer name - R0913 # Too many arguments + R0913, # Too many arguments + R0914 # Too many local variables -[MASTER] -ignore-paths = ^tests/ # Ignore the tests folder From 01e7b01aec1c8975b74af43d7a4aa6169d785cbc Mon Sep 17 00:00:00 2001 From: Mathias G Date: Wed, 11 Oct 2023 14:00:16 +0200 Subject: [PATCH 19/19] Linter fix --- .../SAP/gridview_util.py | 69 +++++-------------- .../SAP/multi_session.py | 34 +++++---- .../SAP/opret_kundekontakt.py | 32 ++++----- ITK_dev_shared_components/SAP/sap_login.py | 46 ++++--------- ITK_dev_shared_components/SAP/sap_util.py | 4 +- ITK_dev_shared_components/SAP/tree_util.py | 68 +++++++++--------- tests/test_gridview_util.py | 68 +++++++++++------- tests/test_multi_session.py | 28 +++++--- tests/test_opret_kundekontakt.py | 12 ++-- tests/test_sap_login.py | 49 ++++++++----- tests/test_tree_util.py | 31 ++++++--- 11 files changed, 225 insertions(+), 216 deletions(-) diff --git a/ITK_dev_shared_components/SAP/gridview_util.py b/ITK_dev_shared_components/SAP/gridview_util.py index a78df3e..305e081 100644 --- a/ITK_dev_shared_components/SAP/gridview_util.py +++ b/ITK_dev_shared_components/SAP/gridview_util.py @@ -1,4 +1,4 @@ -"""This module provides static functions to peform common tasks with SAP GuiGridView COM objects.""" +"""This module provides static functions to perform common tasks with SAP GuiGridView COM objects.""" def scroll_entire_table(grid_view, return_to_top=False) -> None: """This function scrolls through the entire table to load all cells. @@ -9,13 +9,13 @@ def scroll_entire_table(grid_view, return_to_top=False) -> None: Returns: _type_: _description_ - """ + """ if grid_view.RowCount == 0 or grid_view.VisibleRowCount == 0: return - + for i in range(0, grid_view.RowCount, grid_view.VisibleRowCount): grid_view.FirstVisibleRow = i - + if return_to_top: grid_view.FirstVisibleRow = 0 @@ -30,7 +30,7 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: Returns: tuple[tuple[str]]: A 2D tuple of all cell values in the gridview. - """ + """ if pre_load: scroll_entire_table(grid_view, True) @@ -45,9 +45,9 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: for c in columns: v = grid_view.GetCellValue(r, c) row_data.append(v) - + output.append(tuple(row_data)) - + return tuple(output) @@ -62,7 +62,7 @@ def get_row(grid_view, row:int, scroll_to_row=False) -> tuple[str]: Returns: tuple[str]: A tuple of the row's data. - """ + """ if scroll_to_row: grid_view.FirstVisibleRow = row @@ -85,7 +85,7 @@ def iterate_rows(grid_view) -> tuple[str]: Yields: tuple[str]: A tuple of the next row's data. - """ + """ row = 0 while row < grid_view.RowCount: @@ -106,7 +106,7 @@ def get_column_titles(grid_view) -> tuple[str]: Returns: tuple[str]: A tuple of the gridview's column titles. - """ + """ return tuple(grid_view.GetColumnTitles(c)[0] for c in grid_view.ColumnOrder) @@ -125,7 +125,7 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int: Returns: int: The index of the first row which column value matches the given value. - """ + """ if column not in grid_view.ColumnOrder: raise ValueError(f"Column '{column}' not in grid_view") @@ -134,14 +134,14 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int: # Only scroll when row isn't visible if not grid_view.FirstVisibleRow <= row <= grid_view.FirstVisibleRow + grid_view.VisibleRowCount-1: grid_view.FirstVisibleRow = row - + if grid_view.GetCellValue(row, column) == value: return row - + return -1 -def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int]: - """Find all indecies of the rows where the given column's value +def find_all_row_indices_by_value(grid_view, column:str, value:str) -> list[int]: + """Find all indices of the rows where the given column's value match the given value. Returns an empty list if no row is found. Args: @@ -153,8 +153,8 @@ def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int ValueError: If the column name doesn't exist in the grid view. Returns: - list[int]: A list of row indecies where the value matches. - """ + list[int]: A list of row indices where the value matches. + """ if column not in grid_view.ColumnOrder: raise ValueError(f"Column '{column}' not in grid_view") @@ -164,39 +164,8 @@ def find_all_row_indecies_by_value(grid_view, column:str, value:str) -> list[int # Only scroll when row isn't visible if not grid_view.FirstVisibleRow <= row <= grid_view.FirstVisibleRow + grid_view.VisibleRowCount-1: grid_view.FirstVisibleRow = row - + if grid_view.GetCellValue(row, column) == value: rows.append(row) - - return rows - - - - -if __name__=='__main__': - import win32com.client - - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine - connection = app.Connections(0) - session = connection.Sessions(0) - - table = session.findById("wnd[0]/usr/cntlGRID1/shellcont/shell") - - rows = find_all_row_indecies_by_value(table, "ZZ_PARTNER", '15879880') - print(rows) - table.setCurrentCell(rows[0], "ZZ_PARTNER") - - # print(get_row(table, 1, True)) - - # scroll_entire_table(table) - - # data = get_all_rows(table) - # print(len(data), len(data[0])) - - # for r in iterate_rows(table): - # print(r) - - # print(get_column_titles(table)) - + return rows diff --git a/ITK_dev_shared_components/SAP/multi_session.py b/ITK_dev_shared_components/SAP/multi_session.py index ea07d40..c7310ee 100644 --- a/ITK_dev_shared_components/SAP/multi_session.py +++ b/ITK_dev_shared_components/SAP/multi_session.py @@ -6,13 +6,14 @@ import threading import math from typing import Callable + import pythoncom import win32com.client import win32gui def run_with_session(session_index:int, func:Callable, args:tuple) -> None: - """Run a function in a sepcific session based on the sessions index. - This function is meant to be run inside a seperate thread. + """Run a function in a specific session based on the sessions index. + This function is meant to be run inside a separate thread. The function must take a session object as its first argument. Note that this function will not spawn the sessions before running, use spawn_sessions to do that. @@ -20,8 +21,8 @@ def run_with_session(session_index:int, func:Callable, args:tuple) -> None: pythoncom.CoInitialize() - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) session = connection.Sessions(session_index) @@ -29,6 +30,7 @@ def run_with_session(session_index:int, func:Callable, args:tuple) -> None: pythoncom.CoUninitialize() + def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: """Run a function in parallel sessions. The function will be run {num_sessions} times with args[i] as input. @@ -41,7 +43,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: for i in range(num_sessions): t = ExThread(target=run_with_session, args=(i, func, args[i])) threads.append(t) - + for t in threads: t.start() for t in threads: @@ -50,6 +52,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: if t.error: raise t.error + def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): """Run a function in parallel batches. This function runs the input function for each set of arguments in args. @@ -63,6 +66,7 @@ def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): batch = args[b:b+num_sessions] run_batch(func, args, len(batch)) + def spawn_sessions(num_sessions=6) -> tuple: """A function to spawn multiple sessions of SAP. This function will attempt to spawn the desired number of sessions. @@ -78,10 +82,10 @@ def spawn_sessions(num_sessions=6) -> tuple: Returns: tuple: A tuple of all currently open sessions. - """ + """ - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) session = connection.Sessions(0) @@ -92,7 +96,7 @@ def spawn_sessions(num_sessions=6) -> tuple: for _ in range(num_sessions - connection.Sessions.count): session.CreateSession() - + # Wait for the sessions to spawn while connection.Sessions.count < num_sessions: time.sleep(0.1) @@ -113,20 +117,22 @@ def spawn_sessions(num_sessions=6) -> tuple: x = i % c * w y = i // c * h win32gui.MoveWindow(hwnd, x, y, w, h, True) - + return sessions -def get_all_SAP_sessions() -> tuple: + +def get_all_SAP_sessions() -> tuple: # pylint: disable=invalid-name """Returns a tuple of all open SAP sessions (on connection index 0). Returns: tuple: A tuple of SAP GuiSession objects. """ - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine connection = app.Connections(0) return tuple(connection.Sessions) + class ExThread(threading.Thread): """A thread with a handle to get an exception raised inside the thread: ExThread.error""" def __init__(self, *args, **kwargs): @@ -138,5 +144,3 @@ def run(self): self._target(*self._args, **self._kwargs) except Exception as e: # pylint: disable=broad-exception-caught self.error = e - - diff --git a/ITK_dev_shared_components/SAP/opret_kundekontakt.py b/ITK_dev_shared_components/SAP/opret_kundekontakt.py index adc8168..f84b235 100644 --- a/ITK_dev_shared_components/SAP/opret_kundekontakt.py +++ b/ITK_dev_shared_components/SAP/opret_kundekontakt.py @@ -1,12 +1,12 @@ -"""This module provides a single function to conviniently peform the action 'opret-kundekontaker' in SAP.""" +"""This module provides a single function to conveniently peform the action 'opret-kundekontaker' in SAP.""" from typing import Literal import win32clipboard from ITK_dev_shared_components.SAP import tree_util -def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, - art:Literal[' ', 'Automatisk', 'Fakturagrundlag', 'Fuldmagt ifm. værge', 'Konverteret', 'Myndighedshenvend.', 'Orientering', 'Returpost', 'Ringeaktivitet', 'Skriftlig henvend.', 'Telefonisk henvend.'], +def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, + art:Literal[' ', 'Automatisk', 'Fakturagrundlag', 'Fuldmagt ifm. værge', 'Konverteret', 'Myndighedshenvend.', 'Orientering', 'Returpost', 'Ringeaktivitet', 'Skriftlig henvend.', 'Telefonisk henvend.'], notat:str, lock=None) -> None: """Creates a kundekontakt on the given FP and aftaler. @@ -41,11 +41,11 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, # Go to editor and paste (lock if multithreaded) session.findById("wnd[0]/usr/subNOTICE:SAPLEENO:1002/btnEENO_TEXTE-EDITOR").press() - if lock: + if lock: lock.acquire() - _setClipboard(notat) + _set_clipboard(notat) session.findById("wnd[0]/tbar[1]/btn[9]").press() - if lock: + if lock: lock.release() # Back and save @@ -53,21 +53,13 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, session.findById("wnd[0]/tbar[0]/btn[11]").press() -def _setClipboard(text:str) -> None: +def _set_clipboard(text:str) -> None: + """Private function to set text to the clipboard. + + Args: + text: Text to set to clipboard. + """ win32clipboard.OpenClipboard() win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text) win32clipboard.CloseClipboard() - - -if __name__ == '__main__': - from ITK_dev_shared_components.SAP import multi_session - from datetime import datetime - - session = multi_session.spawn_sessions(1)[0] - fp = '25564617' - aftaler = ['2291987', '2421562', '2311094'] - art = 'Orientering' - notat = 'Test '+ str(datetime.now()) - - opret_kundekontakter(session, fp, aftaler, 'Automatisk', notat) \ No newline at end of file diff --git a/ITK_dev_shared_components/SAP/sap_login.py b/ITK_dev_shared_components/SAP/sap_login.py index b30c166..8661787 100644 --- a/ITK_dev_shared_components/SAP/sap_login.py +++ b/ITK_dev_shared_components/SAP/sap_login.py @@ -61,7 +61,7 @@ def _wait_for_download(): path = os.path.join(downloads_folder, file) os.startfile(path) return - + time.sleep(0.5) raise TimeoutError(f".SAP file not found in {downloads_folder}") @@ -79,8 +79,8 @@ def login_using_cli(username: str, password: str, client:str='751', system:str=' Raises: TimeoutError: If SAP doesn't start within timeout limit. ValueError: If SAP is unable to log in using the given credentials. - """ - + """ + command_args = [ r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\sapshcut.exe", f"-system={system}", @@ -93,7 +93,7 @@ def login_using_cli(username: str, password: str, client:str='751', system:str=' _wait_for_sap_session(timeout) if not _check_for_splash_screen(): raise ValueError("Unable to log in. Please check username and password.") - + def _wait_for_sap_session(timeout:int) -> None: """Check every second if the SAP Gui scripting engine is available until timeout is reached. @@ -103,7 +103,7 @@ def _wait_for_sap_session(timeout:int) -> None: Raises: TimeoutError: If SAP doesn't start within timeout limit. - """ + """ for _ in range(timeout): time.sleep(1) try: @@ -123,7 +123,7 @@ def _check_for_splash_screen() -> bool: """ session = multi_session.get_all_SAP_sessions()[0] image = session.findById("wnd[0]/usr/cntlIMAGE_CONTAINER/shellcont/shell/shellcont[1]/shell", False) - + return image is not None def change_password(username:str, old_password:str, new_password:str, @@ -145,16 +145,16 @@ def change_password(username:str, old_password:str, new_password:str, ValueError: If the current credentials are not valid or if the password can't be changed. ValueError: If the new password is not valid. - """ - + """ + subprocess.Popen(r"C:\Program Files (x86)\SAP\FrontEnd\SAPgui\saplogon.exe") #pylint: disable=consider-using-with # Wait for SAP Logon to open for _ in range(timeout): time.sleep(1) try: - SAP = win32com.client.GetObject("SAPGUI") - app = SAP.GetScriptingEngine + sap = win32com.client.GetObject("SAPGUI") + app = sap.GetScriptingEngine app.OpenConnection(system) break except pywintypes.com_error: @@ -164,7 +164,6 @@ def change_password(username:str, old_password:str, new_password:str, session = multi_session.get_all_SAP_sessions()[0] - # Enter credentials session.findById("wnd[0]/usr/txtRSYST-MANDT").text = client session.findById("wnd[0]/usr/txtRSYST-BNAME").text = username @@ -174,37 +173,22 @@ def change_password(username:str, old_password:str, new_password:str, # Check status bar status_bar = session.findById("wnd[0]/sbar") if status_bar.MessageType != 'S': + text = status_bar.Text kill_sap() - raise ValueError(f"Password change was blocked: {status_bar.Text}") + raise ValueError(f"Password change was blocked: {text}") # Enter new password session.findById("wnd[1]/usr/pwdRSYST-NCODE").text = new_password session.findById("wnd[1]/usr/pwdRSYST-NCOD2").text = new_password session.findById("wnd[1]/tbar[0]/btn[0]").press() - + if not _check_for_splash_screen(): kill_sap() raise ValueError("New password couldn't be set. Please check password requirements.") - + kill_sap() - + def kill_sap(): """Kills all SAP processes currently running.""" os.system("taskkill /F /IM saplogon.exe > NUL 2>&1") - - -if __name__=="__main__": - # user = "az12345" - # password = "Hunter2" - # login_using_portal(user, password) - # login_using_cli(user, password) - # change_password(username, password, "Hunter3") - kill_sap() - - username, password = os.environ['SAP Login'].split(';') - login_using_cli(username, password) - - - - diff --git a/ITK_dev_shared_components/SAP/sap_util.py b/ITK_dev_shared_components/SAP/sap_util.py index 698d17a..8235c07 100644 --- a/ITK_dev_shared_components/SAP/sap_util.py +++ b/ITK_dev_shared_components/SAP/sap_util.py @@ -1,7 +1,7 @@ """This module provides miscellaneous static functions to peform common tasks in SAP.""" def print_all_descendants(container, max_depth=-1, indent=0): - """Prints the object and all of its decendants recursivly + """Prints the object and all of its descendants recursively to the console. Args: @@ -32,4 +32,4 @@ def print_all_descendants(container, max_depth=-1, indent=0): usr = session.FindById('/app/con[0]/ses[0]/wnd[0]/usr') - print_all_descendants(usr) \ No newline at end of file + print_all_descendants(usr) diff --git a/ITK_dev_shared_components/SAP/tree_util.py b/ITK_dev_shared_components/SAP/tree_util.py index 034186e..71730c9 100644 --- a/ITK_dev_shared_components/SAP/tree_util.py +++ b/ITK_dev_shared_components/SAP/tree_util.py @@ -2,70 +2,70 @@ def get_node_key_by_text(tree, text: str, fuzzy=False) -> str: """Get the node key of a node based on its text. - tree: A SAP GuiTree object. - text: The text to search for. - fuzzy: Whether to check if the node text just contains the search text. + + Args: + tree: A SAP GuiTree object. + text: The text to search for. + fuzzy: Whether to check if the node text just contains the search text. + + Raises: + ValueError: If no node is found with the given text. + + Returns: + str: The node key of the found node. """ for key in tree.GetAllNodeKeys(): t = tree.GetNodeTextByKey(key) if t == text or (fuzzy and text in t): return key - + raise ValueError(f"No node with the text '{text}' was found.") + def get_item_by_text(tree, text: str, fuzzy=False) -> tuple[str,str]: """Get the node key and item name of an item based on its text. - tree: A SAP GuiTree object. - text: The text to search for. - fuzzy: Whether to check if the item text just contains the search text. + + Args: + tree: A SAP GuiTree object. + text: The text to search for. + fuzzy: Whether to check if the item text just contains the search text. + + Raises: + ValueError: If no tem is found with the given text. + + Returns: + tuple[str,str]: The node key and item name of the found item. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): t = tree.GetItemText(key, name) - + if t == text or (fuzzy and text in t): return (key, name) - + raise ValueError(f"No item with the text '{text}' was found.") + def check_all_check_boxes(tree) -> None: """Find and check all checkboxes in the tree. - tree: A SAP GuiTree object. + + Args: + tree: A SAP GuiTree object. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): if tree.GetItemType(key, name) == 3: tree.ChangeCheckBox(key, name, True) + def uncheck_all_check_boxes(tree) -> None: """Find and uncheck all checkboxes in the tree. - tree: A SAP GuiTree object. + + Args: + tree: A SAP GuiTree object. """ for key in tree.GetAllNodeKeys(): for name in tree.GetColumnNames(): if tree.GetItemType(key, name) == 3: tree.ChangeCheckBox(key, name, False) - - - - -if __name__ == '__main__': - from ITK_dev_shared_components.SAP import multi_session - - session = multi_session.spawn_sessions(1)[0] - - tree = session.findById('/app/con[0]/ses[0]/wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]') - - # print([tree.GetNodeTextByKey(key) for key in tree.GetAllNodeKeys()]) - # print([tree.GetItemText(key, '&Hierarchy') for key in tree.GetAllNodeKeys()]) - - # print(list(tree.GetColumnNames())) - - # print(tree.GetItemText(' 2', '&Hierarchy')) - - # key, name = get_item_by_text(tree, '2291987', True) - # tree.ChangeCheckBox(key, name, True) - - check_all_check_boxes(tree) - uncheck_all_check_boxes(tree) \ No newline at end of file diff --git a/tests/test_gridview_util.py b/tests/test_gridview_util.py index c0c3a99..ebdc1b9 100644 --- a/tests/test_gridview_util.py +++ b/tests/test_gridview_util.py @@ -1,10 +1,14 @@ +"""Tests relating to the module SAP.gridview_util.""" + import unittest import os from ITK_dev_shared_components.SAP import gridview_util, sap_login, multi_session -class test_gridview_util(unittest.TestCase): +class TestGridviewUtil(unittest.TestCase): + """Tests relating to the module SAP.gridview_util.""" @classmethod def setUpClass(cls): + """Launch SAP and navigate to fmcacov on FP 25564617 (Test person).""" sap_login.kill_sap() user, password = os.environ['SAP Login'].split(';') @@ -12,66 +16,78 @@ def setUpClass(cls): session = multi_session.get_all_SAP_sessions()[0] session.startTransaction("fmcacov") session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" - session.findById("wnd[0]").sendVKey(0) + session.findById("wnd[0]").sendVKey(0) @classmethod def tearDownClass(cls): sap_login.kill_sap() - + def setUp(self) -> None: + # Find SAP gridview (table) object for testing session = multi_session.get_all_SAP_sessions()[0] self.table = session.findById("wnd[0]/usr/tabsDATA_DISP/tabpDATA_DISP_FC1/ssubDATA_DISP_SCA:RFMCA_COV:0202/cntlRFMCA_COV_0100_CONT5/shellcont/shell") def test_scroll_entire_table(self): + """Test scroll_entire_table. Assume success if no errors.""" gridview_util.scroll_entire_table(self.table) gridview_util.scroll_entire_table(self.table, True) def test_get_all_rows(self): + """Test get all rows of table. + Assume success if any rows and columns are loaded. + """ result = gridview_util.get_all_rows(self.table) self.assertGreater(len(result), 0) self.assertGreater(len(result[0]), 0) - + def test_get_row(self): + """Test getting a single row. + Assume success if any columns are loaded. + """ result = gridview_util.get_row(self.table, 0, False) self.assertGreater(len(result), 0) result = gridview_util.get_row(self.table, 0, True) self.assertGreater(len(result), 0) - + def test_iterate_rows(self): + """Test iterating through all rows. + Assume success if any columns are loaded for each row. + """ for row in gridview_util.iterate_rows(self.table): self.assertGreater(len(row), 0) - + def test_get_column_titles(self): + """Test getting column titles. + Assume success if any titles are loaded. + """ result = gridview_util.get_column_titles(self.table) self.assertGreater(len(result), 0) - + def test_find_row_index_by_value(self): - result = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Test Deltrans") - self.assertNotEqual(result, -1) + """Test finding a single row by column value.""" + # Test finding an actual value. + index = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Test Deltrans") + self.assertNotEqual(index, -1) - result = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Foo") - self.assertEqual(result, -1) + # Test NOT finding a wrong value. + index = gridview_util.find_row_index_by_value(self.table, "TXTU2", "Foo") + self.assertEqual(index, -1) + # Test error on wrong column name. with self.assertRaises(ValueError): gridview_util.find_row_index_by_value(self.table, "Foo", "Bar") - def test_find_all_row_indecies_by_value(self): - result = gridview_util.find_all_row_indecies_by_value(self.table, "TXTU2", "Gebyr") - self.assertGreater(len(result), 0) + """Test finding all rows by column value.""" + # Test finding an actual value. + indices = gridview_util.find_all_row_indices_by_value(self.table, "TXTU2", "Gebyr") + self.assertGreater(len(indices), 0) - result = gridview_util.find_all_row_indecies_by_value(self.table, "TXTU2", "Foo") - self.assertEqual(len(result), 0) + # Test NOT finding a wrong value. + indices = gridview_util.find_all_row_indices_by_value(self.table, "TXTU2", "Foo") + self.assertEqual(len(indices), 0) + # Test error on wrong column name. with self.assertRaises(ValueError): - gridview_util.find_all_row_indecies_by_value(self.table, "Foo", "Bar") - - - - - - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + gridview_util.find_all_row_indices_by_value(self.table, "Foo", "Bar") diff --git a/tests/test_multi_session.py b/tests/test_multi_session.py index 4deb632..db55199 100644 --- a/tests/test_multi_session.py +++ b/tests/test_multi_session.py @@ -1,9 +1,13 @@ +"""Tests relating to the module SAP.multi_session.""" + import unittest import os import threading from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt -class test_multi_session(unittest.TestCase): +class TestMultiSession(unittest.TestCase): + """Tests relating to the module SAP.multi_session.""" + def setUp(self): sap_login.kill_sap() user, password = os.environ['SAP Login'].split(';') @@ -12,13 +16,16 @@ def setUp(self): def tearDown(self): sap_login.kill_sap() - def test_spawn_sessions(self): + """Test spawning of multiple sessions. + It should only be possible to spawn between 1-6 sessions. + Also test getting already open sessions. + """ with self.assertRaises(ValueError): multi_session.spawn_sessions(0) with self.assertRaises(ValueError): - multi_session.spawn_sessions(7) + multi_session.spawn_sessions(7) sessions = multi_session.spawn_sessions(1) self.assertEqual(len(sessions), 1) @@ -29,14 +36,16 @@ def test_spawn_sessions(self): sessions = multi_session.get_all_SAP_sessions() self.assertEqual(len(sessions), 6) - def test_run_batches(self): - # This also tests: - # run_batch, run_with_session, ExThread + """Test running a task in parallel in multiple sessions. + This also tests: + run_batch, run_with_session, ExThread + """ + num_sessions = 6 multi_session.spawn_sessions(num_sessions) - + #Data lock = threading.Lock() @@ -46,12 +55,13 @@ def test_run_batches(self): # Test with 12 cases multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data, num_sessions) - + # Test with 5 cases multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:5], num_sessions) # Test with 1 case multi_session.run_batches(opret_kundekontakt.opret_kundekontakter, data[0:1], num_sessions) + if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_opret_kundekontakt.py b/tests/test_opret_kundekontakt.py index 81dbb52..7452b30 100644 --- a/tests/test_opret_kundekontakt.py +++ b/tests/test_opret_kundekontakt.py @@ -1,9 +1,11 @@ +"""Test relating to the module SAP.opret_kundekontakt.""" + import unittest import os -import threading from ITK_dev_shared_components.SAP import sap_login, multi_session, opret_kundekontakt -class test_opret_kundekontakt(unittest.TestCase): +class TestOpretKundekontakt(unittest.TestCase): + """Test relating to the module SAP.opret_kundekontakt.""" def setUp(self): sap_login.kill_sap() user, password = os.environ['SAP Login'].split(';') @@ -14,9 +16,10 @@ def tearDown(self): def test_opret_kundekontakt(self): + """Test the function opret_kundekontakter.""" fp = "25564617" aftaler = ("2291987", "1990437", "1473781") - + session = multi_session.get_all_SAP_sessions()[0] # Test with 3 aftaler @@ -28,7 +31,6 @@ def test_opret_kundekontakt(self): # Test with 0 aftaler opret_kundekontakt.opret_kundekontakter(session, fp, None, 'Returpost', "Test 3") - if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_sap_login.py b/tests/test_sap_login.py index bb709b5..bdad59b 100644 --- a/tests/test_sap_login.py +++ b/tests/test_sap_login.py @@ -1,39 +1,56 @@ +"""Tests relating to the module SAP.sap_login.""" + import unittest -import time -import os +from tkinter import simpledialog + from ITK_dev_shared_components.SAP import sap_login -class test_sap_login(unittest.TestCase): +class TestSapLogin(unittest.TestCase): + """Tests relating to the module SAP.sap_login.""" + + @classmethod + def setUpClass(cls) -> None: + """Show popups that asks for username, password and new password + used in the following tests. + """ + cls.username = simpledialog.askstring("Enter username", "Enter the SAP username to be used in tests.") + cls.password = simpledialog.askstring("Enter password", "Enter the SAP password to be used in tests.") + cls.new_password = simpledialog.askstring("Enter password", "Enter the new password to be used in tests.\nRemember to write down the new password!\nLeave empty to skip test_change_password.") + def setUp(self) -> None: sap_login.kill_sap() - + def tearDown(self) -> None: sap_login.kill_sap() def test_login_with_cli(self): - user, password = os.environ['SAP Login'].split(';') - sap_login.login_using_cli(user, password) + """Test login using the SAP cli interface. + Username and password is found in a environment variable. + """ + sap_login.login_using_cli(self.username, self.password) sap_login.kill_sap() with self.assertRaises(ValueError): sap_login.login_using_cli("Foo", "Bar") - - @unittest.skip("Should be run manually") def test_change_password(self): - username = "az12345" - password = "Hunter2" - new_password = "Hunter3" + """Test the function change password. + Due to a limit in SAP you can only run this function once per day. + If no new_password is entered in the setup + """ + if not self.new_password: + raise ValueError("Test not run because new_password was missing.") + + sap_login.change_password(self.username, self.password, self.new_password) + self.password = self.new_password - sap_login.change_password(username, password, new_password) - with self.assertRaises(ValueError): - sap_login.change_password(username, "Foo", new_password) + sap_login.change_password(self.username, "Foo", self.new_password) with self.assertRaises(ValueError): - sap_login.change_password(username, password, "") + sap_login.change_password(self.username, self.password, "") if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/test_tree_util.py b/tests/test_tree_util.py index 9225fe3..4375cd8 100644 --- a/tests/test_tree_util.py +++ b/tests/test_tree_util.py @@ -1,10 +1,15 @@ +"""Tests relating to the module SAP.tree_util.""" + import unittest import os + from ITK_dev_shared_components.SAP import tree_util, sap_login, multi_session -class test_tree_util(unittest.TestCase): +class TestTreeUtil(unittest.TestCase): + """Tests relating to the module SAP.tree_util.""" @classmethod def setUpClass(cls): + """Launch SAP and navigate to fmcacov on FP 25564617.""" sap_login.kill_sap() user, password = os.environ['SAP Login'].split(';') @@ -12,15 +17,19 @@ def setUpClass(cls): session = multi_session.get_all_SAP_sessions()[0] session.startTransaction("fmcacov") session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" - session.findById("wnd[0]").sendVKey(0) + session.findById("wnd[0]").sendVKey(0) @classmethod def tearDownClass(cls): sap_login.kill_sap() - + def test_get_node_key_by_text(self): + """Test get_node_key_by_test. + Test that strict search and fuzzy search works + and throws errors on nonsense input. + """ session = multi_session.get_all_SAP_sessions()[0] tree = session.findById("wnd[0]/shellcont/shell") @@ -32,12 +41,16 @@ def test_get_node_key_by_text(self): with self.assertRaises(ValueError): tree_util.get_node_key_by_text(tree, "Foo Bar") - + with self.assertRaises(ValueError): tree_util.get_node_key_by_text(tree, "Foo Bar", True) - + def test_get_item_by_text(self): + """Test get_item_by_text. + Test that strict search and fuzzy search works + and throws errors on nonsense input. + """ session = multi_session.get_all_SAP_sessions()[0] tree = session.findById("wnd[0]/shellcont/shell") @@ -52,13 +65,15 @@ def test_get_item_by_text(self): with self.assertRaises(ValueError): tree_util.get_item_by_text(tree, "Foo Bar", True) - + def test_check_uncheck_all_check_boxes(self): + """Test check_all_check_boxes and uncheck_all_check_boxes.""" + # Open popup with tree containing many checkboxes. session = multi_session.get_all_SAP_sessions()[0] session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") - + # Test on tree with checkboxes tree = session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]") tree_util.check_all_check_boxes(tree) @@ -73,4 +88,4 @@ def test_check_uncheck_all_check_boxes(self): if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main()