diff --git a/.github/workflows/Changelog.yml b/.github/workflows/Changelog.yml new file mode 100644 index 0000000..73e9d7d --- /dev/null +++ b/.github/workflows/Changelog.yml @@ -0,0 +1,21 @@ +name: Check Changelog + +on: pull_request + +jobs: + changelog: + runs-on: ubuntu-latest + name: Changelog should be updated + strategy: + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Git fetch + run: git fetch + + - name: Check that changelog has been updated. + run: git diff --exit-code origin/${{ github.base_ref }} -- changelog.md && exit 1 || exit 0 \ No newline at end of file diff --git a/.github/workflows/pylint.yml b/.github/workflows/Linting.yml similarity index 69% rename from .github/workflows/pylint.yml rename to .github/workflows/Linting.yml index 0bf6485..5bfc7d2 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/Linting.yml @@ -1,4 +1,4 @@ -name: Pylint +name: Linting on: [push] @@ -8,17 +8,25 @@ jobs: strategy: matrix: python-version: ["3.11"] + fail-fast: false steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install pylint + pip install flake8 pip install . - - name: Analysing the code with pylint + + - name: Analyzing the code with pylint run: | pylint --rcfile=.pylintrc $(git ls-files '*.py') + + - name: Analyzing the code with flake8 + run: | + flake8 --extend-ignore=E501,E251 $(git ls-files '*.py') \ No newline at end of file diff --git a/.gitignore b/.gitignore index b745cbd..4790cbd 100644 --- a/.gitignore +++ b/.gitignore @@ -123,7 +123,7 @@ celerybeat.pid # Environments .env -.venv +.venv* env/ venv/ ENV/ diff --git a/README.md b/README.md index d46f5b5..a6a3fa7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,39 @@ # itk-dev-shared-components -https://pypi.org/project/ITK-dev-shared-components/ +## Links -pip install ITK-dev-shared-components +[Documentation](https://itk-dev-rpa.github.io/itk-dev-shared-components-docs/) + +[Pypi](https://pypi.org/project/ITK-dev-shared-components/) + +## Installation + +``` +pip install itk-dev-shared-components +``` + +## Intro + +This python library contains helper modules for RPA development. +It's based on the need of [ITK Dev](https://itk.aarhus.dk/), but it has been +generalized to be useful for others as well. + +## Integrations + +### SAP Gui + +Helper functions for using SAP Gui. A few examples include: + +- Login to SAP. +- Handling multiple sessions in multiple threads. +- Convenience functions for gridviews and trees. + +### Microsoft Graph + +Helper functions for using Microsoft Graph to read emails from shared inboxes. +Some examples are: + +- Authentication using username and password. +- List and get emails. +- Get attachment data. +- Move and delete emails. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..94c0d6e --- /dev/null +++ b/changelog.md @@ -0,0 +1,37 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.1.0] - 2023-11-28 + +### Changed + +- sap.opret_kundekontakt: Function 'opret_kundekontakter' made more stable. +- sap.multi_session: Function 'spawn_session' is no longer hardcoded to 1080p screen size. +- sap.multi_session: Arrange session windows moved to separate function. +- Bunch o' linting. +- run_tests.bat: Dedicated test venv. +- readme: Updated readme. + +### Added + +- Changelog! +- pylint.yml: Flake8 added. + +### Fixed + +- sap.multi_session: Critical bug in 'run_batches'. +- tests.sap.login: Change environ 'SAP Login' for later tests. + +## [1.0.0] - 2023-11-14 + +- Initial release + +[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/1.1.0...HEAD +[1.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.1.0 +[1.0.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.0.0 diff --git a/itk_dev_shared_components/graph/authentication.py b/itk_dev_shared_components/graph/authentication.py index cd66de6..81c9a63 100644 --- a/itk_dev_shared_components/graph/authentication.py +++ b/itk_dev_shared_components/graph/authentication.py @@ -3,6 +3,7 @@ import msal + # pylint: disable-next=too-few-public-methods class GraphAccess: """An object that handles access to the Graph api. @@ -45,7 +46,7 @@ def authorize_by_username_password(username: str, password: str, *, client_id: s password: The password of the user. client_id: The Graph API client id in 8-4-4-12 format. tenant_id: The Graph API tenant id in 8-4-4-12 format. - + Returns: GraphAccess: The GraphAccess object used to authorize Graph access. """ diff --git a/itk_dev_shared_components/graph/mail.py b/itk_dev_shared_components/graph/mail.py index 5bd120a..d822b8e 100644 --- a/itk_dev_shared_components/graph/mail.py +++ b/itk_dev_shared_components/graph/mail.py @@ -162,15 +162,15 @@ def get_attachment_data(attachment: Attachment, graph_access: GraphAccess) -> io """ email = attachment.email endpoint = f"https://graph.microsoft.com/v1.0/users/{email.user}/messages/{email.id}/attachments/{attachment.id}/$value" - response = _get_request(endpoint, graph_access) + response = _get_request(endpoint, graph_access) data_bytes = response.content return io.BytesIO(data_bytes) -def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, well_known_folder: bool=False) -> None: +def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, well_known_folder: bool = False) -> None: """Move an email to another folder under the same user. If well_known_folder is true, the folder path is assumed to be a well defined folder. - See https://learn.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0 + See https://learn.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0 for a list of well defined folder names. Args: @@ -208,7 +208,7 @@ def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, wel email.id = new_id -def delete_email(email: Email, graph_access: GraphAccess, *, permanent: bool=False) -> None: +def delete_email(email: Email, graph_access: GraphAccess, *, permanent: bool = False) -> None: """Delete an email from the mailbox. If permanent is true the email is completely removed from the user's mailbox. If permanent is false the email is instead moved to the Deleted Items folder. @@ -235,7 +235,7 @@ def delete_email(email: Email, graph_access: GraphAccess, *, permanent: bool=Fal def _find_folder(response: dict, target_folder: str) -> str: - """Find the target folder in + """Find the target folder in Args: response: The json dict of the HTTP response. @@ -300,7 +300,7 @@ def _get_request(endpoint: str, graph_access: GraphAccess) -> requests.models.Re Returns: Response: The response object of the GET request. - + Raises: HTTPError: Any errors raised while performing GET request. """ diff --git a/itk_dev_shared_components/sap/gridview_util.py b/itk_dev_shared_components/sap/gridview_util.py index 8889c74..c32da0f 100644 --- a/itk_dev_shared_components/sap/gridview_util.py +++ b/itk_dev_shared_components/sap/gridview_util.py @@ -1,5 +1,6 @@ """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. @@ -23,7 +24,7 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: Args: grid_view: A SAP GuiGridView object. pre_load: Whether to first scroll through the table to load all values. - If a row hasn't been loaded before reading, the row data will be empty. + If a row hasn't been loaded before reading, the row data will be empty. Returns: tuple[tuple[str]]: A 2D tuple of all cell values in the gridview. @@ -48,14 +49,14 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]: return tuple(output) -def get_row(grid_view, row:int, scroll_to_row=False) -> tuple[str]: +def get_row(grid_view, row: int, scroll_to_row=False) -> tuple[str]: """Returns the data of a single row. Args: grid_view: A SAP GuiGridView object. row: The zero-based index of the row. scroll_to_row: Whether to scroll to the row before reading the data. - This ensures the data of the row has been loaded before reading. + This ensures the data of the row has been loaded before reading. Returns: tuple[str]: A tuple of the row's data. @@ -108,7 +109,7 @@ def get_column_titles(grid_view) -> tuple[str]: return tuple(grid_view.GetColumnTitles(c)[0] for c in grid_view.ColumnOrder) -def find_row_index_by_value(grid_view, column:str, value:str) -> int: +def find_row_index_by_value(grid_view, column: str, value: str) -> int: """Find the index of the first row where the given column's value matches the given value. Returns -1 if no row is found. @@ -137,7 +138,8 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int: return -1 -def find_all_row_indices_by_value(grid_view, column:str, value:str) -> list[int]: + +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. diff --git a/itk_dev_shared_components/sap/multi_session.py b/itk_dev_shared_components/sap/multi_session.py index b3d01de..4bfbc71 100644 --- a/itk_dev_shared_components/sap/multi_session.py +++ b/itk_dev_shared_components/sap/multi_session.py @@ -10,15 +10,16 @@ import pythoncom import win32com.client import win32gui +import win32api -def run_with_session(session_index:int, func:Callable, args:tuple) -> None: + +def run_with_session(session_index: int, func: Callable, args: tuple) -> None: """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. """ - pythoncom.CoInitialize() sap = win32com.client.GetObject("SAPGUI") @@ -31,17 +32,24 @@ 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: +def run_batch(func: Callable, args: tuple[tuple]) -> None: """Run a function in parallel sessions. - The function will be run {num_sessions} times with args[i] as input. + A number of threads equal to the length of args will be spawned. 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. + + Args: + func: A callable function to run in the threads. + args: A tuple of tuples containing arguments to be passed to func. + + Raises: + Exception: Any exception raised in any of the threads. """ threads = [] - for i in range(num_sessions): - t = ExThread(target=run_with_session, args=(i, func, args[i])) + for i, arg in enumerate(args): + t = ExThread(target=run_with_session, args=(i, func, arg)) threads.append(t) for t in threads: @@ -53,7 +61,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None: raise t.error -def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): +def run_batches(func: Callable, args: tuple[tuple], num_sessions: int = 6) -> None: """Run a function in parallel batches. This function runs the input function for each set of arguments in args. The function will be run in parallel batches of size {num_sessions}. @@ -64,7 +72,7 @@ def run_batches(func:Callable, args:tuple[tuple], num_sessions=6): for b in range(0, len(args), num_sessions): batch = args[b:b+num_sessions] - run_batch(func, args, len(batch)) + run_batch(func, batch) def spawn_sessions(num_sessions=6) -> list: @@ -100,14 +108,39 @@ def spawn_sessions(num_sessions=6) -> list: while connection.Sessions.count < num_sessions: time.sleep(0.1) - sessions = tuple(connection.Sessions) + arrange_sessions() + + return tuple(connection.Sessions) + + +def get_all_sap_sessions() -> tuple: + """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 + connection = app.Connections(0) + return tuple(connection.Sessions) + + +def arrange_sessions(): + """Take all toplevel windows of currently open SAP sessions + and arrange them equally on the screen. + """ + sessions = get_all_sap_sessions() num_sessions = len(sessions) # 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 + screen_width = win32api.GetSystemMetrics(0) + screen_height = win32api.GetSystemMetrics(1) + + w = screen_width // c + h = screen_height // r for i, session in enumerate(sessions): window = session.findById("wnd[0]") @@ -117,20 +150,6 @@ def spawn_sessions(num_sessions=6) -> list: y = i // c * h win32gui.MoveWindow(hwnd, x, y, w, h, True) - return sessions - - -def get_all_sap_sessions() -> tuple: - """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 - 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""" @@ -141,5 +160,5 @@ def __init__(self, *args, **kwargs): def run(self): try: self._target(*self._args, **self._kwargs) - except Exception as e: # pylint: disable=broad-exception-caught + 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 306abff..37fa051 100644 --- a/itk_dev_shared_components/sap/opret_kundekontakt.py +++ b/itk_dev_shared_components/sap/opret_kundekontakt.py @@ -1,22 +1,28 @@ """This module provides a single function to conveniently perform the action 'opret-kundekontaker' in SAP.""" from typing import Literal +from datetime import date + 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.'], - notat:str, lock=None) -> None: +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. Args: - session (COM Object): The SAP session to preform the action. - fp (str): The forretningspartner number. - aftaler (list[str] | None): A list of aftaler to put the kundekontakt on. If empty or None the kundekontakt will be created on fp-level. - art (str): The art of the kundekontakt. - notat (str): The text of the kundekontakt. - lock (threading.Lock, optional): A threading.Lock object to allow this function to run properly when multithreaded. Defaults to None. + session: The SAP session to preform the action. + fp: The forretningspartner number. + aftaler: A list of aftaler to put the kundekontakt on. If empty or None the kundekontakt will be created on fp-level. + art: The art of the kundekontakt. + notat: The text of the kundekontakt. + lock: A threading.Lock object to allow this function to run properly when multithreaded. Defaults to None. + + Raises: + RuntimeError: If the kundekontakt wasn't created. """ session.StartTransaction('fmcacov') @@ -28,6 +34,17 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") + # When running multithreaded the toolbar buttons in the pop-up doesn't appear sometimes. + # Open and close the pop-up until they appear + for _ in range(10): + if session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[0]").ButtonCount == 2: + break + session.findById("wnd[1]").close() + session.findById("wnd[0]/shellcont/shell").nodeContextMenu("GP0000000001") + session.findById("wnd[0]/shellcont/shell").selectContextMenuItem("FLERE") + else: + raise RuntimeError("The toolbar buttons in 'Opret kundekontakter-flere' didn't show up after 10 tries.") + # Vælg aftaler if aftaler: aftale_tree = session.findById("wnd[1]/usr/cntlCONTAINER_PSOBKEY/shellcont/shell/shellcont[1]/shell[1]") @@ -52,8 +69,10 @@ def opret_kundekontakter(session, fp:str, aftaler:list[str] | None, session.findById("wnd[0]/tbar[0]/btn[3]").press() session.findById("wnd[0]/tbar[0]/btn[11]").press() + _confirm_kundekontakt(session, notat, art) + -def _set_clipboard(text:str) -> None: +def _set_clipboard(text: str) -> None: """Private function to set text to the clipboard. Args: @@ -63,3 +82,26 @@ def _set_clipboard(text:str) -> None: win32clipboard.EmptyClipboard() win32clipboard.SetClipboardText(text) win32clipboard.CloseClipboard() + + +def _confirm_kundekontakt(session, notat: str, art: str): + """Confirm the kundekontakt was created + Compare the top 10 kundekontakter in the table with the input of this function + Compare date, art and (up to) the first 10 letters of the notat + """ + session.findById("wnd[0]/usr/tabsDATA_DISP/tabpDATA_DISP_FC3").select() + table = session.findById("wnd[0]/usr/tabsDATA_DISP/tabpDATA_DISP_FC3/ssubDATA_DISP_SCA:RFMCA_COV:0204/cntlRFMCA_COV_0100_CONT3/shellcont/shell") + + rows = min(10, table.RowCount) + + for row in range(rows): + kundekontakt_date = table.GetCellValue(row, "DATE") + kontaktart = table.GetCellValue(row, "ZZ_KONTAKTART") + kontakttekst = table.GetCellValue(row, "ZZ_TEXT") + + length_to_compare = min([10, len(notat), len(kontakttekst)]) + + if (date.today().strftime("%d.%m.%Y") == kundekontakt_date and art == kontaktart and notat[:length_to_compare] == kontakttekst[:length_to_compare]): + break + else: + raise RuntimeError("The kundekontakt wasn't found in the kontakt-table after creation.") diff --git a/itk_dev_shared_components/sap/sap_login.py b/itk_dev_shared_components/sap/sap_login.py index 27ffc78..7172f0b 100644 --- a/itk_dev_shared_components/sap/sap_login.py +++ b/itk_dev_shared_components/sap/sap_login.py @@ -14,7 +14,7 @@ from itk_dev_shared_components.sap import multi_session -def login_using_portal(username:str, password:str): +def login_using_portal(username: str, password: str): """Open KMD Portal in Edge, login and start SAP GUI. Args: user: KMD Portal username. @@ -25,7 +25,7 @@ def login_using_portal(username:str, password:str): driver.get('https://portal.kmd.dk/irj/portal') driver.maximize_window() - #Login + # Login user_field = driver.find_element(By.ID, 'logonuidfield') pass_field = driver.find_element(By.ID, 'logonpassfield') login_button = driver.find_element(By.ID, 'buttonLogon') @@ -35,11 +35,11 @@ def login_using_portal(username:str, password:str): pass_field.send_keys(password) login_button.click() - #Opus + # Opus mine_genveje = driver.find_element(By.CSS_SELECTOR, "div[title='Mine Genveje']") mine_genveje.click() - #Wait for download and launch file + # Wait for download and launch file _wait_for_download() driver.quit() @@ -63,7 +63,7 @@ def _wait_for_download(): raise TimeoutError(f".sap file not found in {downloads_folder}") -def login_using_cli(username: str, password: str, client:str='751', system:str='P02', timeout:int=10) -> 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: AZ username @@ -90,7 +90,7 @@ def login_using_cli(username: str, password: str, client:str='751', system:str=' raise ValueError("Unable to log in. Please check username and password.") -def _wait_for_sap_session(timeout:int) -> None: +def _wait_for_sap_session(timeout: int) -> None: """Check every second if the SAP Gui scripting engine is available until timeout is reached. Args: timeout: The time in seconds to wait for SAP Logon to start. Defaults to 10. @@ -120,10 +120,10 @@ def _check_for_splash_screen() -> bool: 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) -> None: +def change_password(username: str, old_password: str, new_password: str, + client: str = '751', + system: str = '...KMD OPUS Produktion [P02]', + timeout: int = 10) -> None: """Change the password of a user in SAP Gui. Closes SAP when done. Args: username: The username of the user. @@ -136,10 +136,9 @@ def change_password(username:str, old_password:str, new_password:str, TimeoutError: If the connection couldn't be established within the timeout limit. 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 + 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): diff --git a/itk_dev_shared_components/sap/sap_util.py b/itk_dev_shared_components/sap/sap_util.py index 70d022d..fb56dd3 100644 --- a/itk_dev_shared_components/sap/sap_util.py +++ b/itk_dev_shared_components/sap/sap_util.py @@ -1,4 +1,5 @@ -"""This module provides miscellaneous static functions to peform common tasks in SAP.""" +"""This module provides miscellaneous static functions to perform common tasks in SAP.""" + def print_all_descendants(container, max_depth=-1, indent=0): """Prints the object and all of its descendants recursively @@ -7,7 +8,7 @@ def print_all_descendants(container, max_depth=-1, indent=0): Args: container: A SAP GuiContainer object. max_depth: The maximum depth of the recursive search. Defaults to -1. - indent: The indentation level of the printed text. + indent: The indentation level of the printed text. This increases for each level of the recursion. Defaults to 0. """ indent_text = ' |'*indent @@ -18,8 +19,9 @@ def print_all_descendants(container, max_depth=-1, indent=0): print(indent_text, child.Id) if (hasattr(child, 'Children') and child.Children is not None - and len(child.Children) != 0 - and (indent < max_depth or max_depth == -1)): + and len(child.Children) != 0 + and (indent < max_depth or max_depth == -1)): + print_all_descendants(child, max_depth, indent+1) else: print(indent_text) diff --git a/itk_dev_shared_components/sap/tree_util.py b/itk_dev_shared_components/sap/tree_util.py index 3eeca44..68883c8 100644 --- a/itk_dev_shared_components/sap/tree_util.py +++ b/itk_dev_shared_components/sap/tree_util.py @@ -1,6 +1,7 @@ """This module provides static functions to perform common tasks with SAP GuiTree COM objects.""" -def get_node_key_by_text(tree, text: str, fuzzy: bool=False) -> str: + +def get_node_key_by_text(tree, text: str, fuzzy: bool = False) -> str: """Get the node key of a node based on its text. Args: @@ -23,17 +24,17 @@ def get_node_key_by_text(tree, text: str, fuzzy: bool=False) -> str: raise ValueError(f"No node with the text '{text}' was found.") -def get_item_by_text(tree, text: str, fuzzy: bool=False) -> tuple[str,str]: +def get_item_by_text(tree, text: str, fuzzy: bool = False) -> tuple[str, str]: """Get the node key and item name of an item based on its 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. """ diff --git a/pyproject.toml b/pyproject.toml index da6691e..93a9271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "itk_dev_shared_components" -version = "1.0.0" +version = "1.1.0" authors = [ { name="ITK Development", email="itk-rpa@mkb.aarhus.dk" }, ] diff --git a/tests/run_tests.bat b/tests/run_tests.bat index c1e6516..142c105 100644 --- a/tests/run_tests.bat +++ b/tests/run_tests.bat @@ -9,12 +9,12 @@ choice /C YN /M "Do you want to reset venv?" if errorlevel 2 ( echo Activating excisting venv... - call .venv\Scripts\activate + call .venv-test\Scripts\activate ) else ( echo Setting up new venv... - python -m venv .venv - call .venv\Scripts\activate + python -m venv .venv-test + call .venv-test\Scripts\activate echo Installing package... pip install . diff --git a/tests/test_graph/test_mail.py b/tests/test_graph/test_mail.py index 681baf2..fbadae3 100644 --- a/tests/test_graph/test_mail.py +++ b/tests/test_graph/test_mail.py @@ -6,6 +6,7 @@ from itk_dev_shared_components.graph import authentication, mail + class EmailTest(unittest.TestCase): """Tests relating to the graph.mail module.""" @classmethod @@ -64,7 +65,6 @@ def test_correct_usage(self): # Move email back mail.move_email(email, self.folder1, self.graph_access) - def test_wrong_usage(self): """Test that raised errors actually get raised.""" with self.assertRaises(ValueError): diff --git a/tests/test_sap/test_gridview_util.py b/tests/test_sap/test_gridview_util.py index b033a44..b4b6029 100644 --- a/tests/test_sap/test_gridview_util.py +++ b/tests/test_sap/test_gridview_util.py @@ -4,7 +4,8 @@ import os from itk_dev_shared_components.sap import gridview_util, sap_login, multi_session -# Some tests might look similiar, and we want this. pylint: disable=duplicate-code +# Some tests might look similar, and we want this. pylint: disable=duplicate-code + class TestGridviewUtil(unittest.TestCase): """Tests relating to the module SAP.gridview_util.""" @@ -35,7 +36,7 @@ def test_scroll_entire_table(self): gridview_util.scroll_entire_table(self.table, True) def test_get_all_rows(self): - """Test get all rows of table. + """Test get all rows of table. Assume success if any rows and columns are loaded. """ result = gridview_util.get_all_rows(self.table) diff --git a/tests/test_sap/test_multi_session.py b/tests/test_sap/test_multi_session.py index 5e89358..9b1dc7f 100644 --- a/tests/test_sap/test_multi_session.py +++ b/tests/test_sap/test_multi_session.py @@ -5,6 +5,7 @@ import threading from itk_dev_shared_components.sap import sap_login, multi_session, opret_kundekontakt + class TestMultiSession(unittest.TestCase): """Tests relating to the module SAP.multi_session.""" @@ -46,7 +47,7 @@ def test_run_batches(self): multi_session.spawn_sessions(num_sessions) - #Data + # Data lock = threading.Lock() data = [ diff --git a/tests/test_sap/test_opret_kundekontakt.py b/tests/test_sap/test_opret_kundekontakt.py index 78b13e3..68334f5 100644 --- a/tests/test_sap/test_opret_kundekontakt.py +++ b/tests/test_sap/test_opret_kundekontakt.py @@ -4,6 +4,7 @@ import os from itk_dev_shared_components.sap import sap_login, multi_session, opret_kundekontakt + class TestOpretKundekontakt(unittest.TestCase): """Test relating to the module SAP.opret_kundekontakt.""" def setUp(self): @@ -14,7 +15,6 @@ def setUp(self): def tearDown(self): sap_login.kill_sap() - def test_opret_kundekontakt(self): """Test the function opret_kundekontakter.""" fp = "25564617" diff --git a/tests/test_sap/test_sap_login.py b/tests/test_sap/test_sap_login.py index 4f4786d..215b994 100644 --- a/tests/test_sap/test_sap_login.py +++ b/tests/test_sap/test_sap_login.py @@ -6,6 +6,7 @@ from itk_dev_shared_components.sap import sap_login + class TestSapLogin(unittest.TestCase): """Tests relating to the module SAP.sap_login.""" @@ -43,7 +44,9 @@ def test_change_password(self): raise unittest.SkipTest("Test not run because new_password was missing.") sap_login.change_password(self.username, self.password, self.new_password) + # Change password for all coming tests self.password = self.new_password + os.environ['SAP Login'] = f"{self.username};{self.password}" with self.assertRaises(ValueError): sap_login.change_password(self.username, "Foo", self.new_password) diff --git a/tests/test_sap/test_tree_util.py b/tests/test_sap/test_tree_util.py index eaadf33..a2b208b 100644 --- a/tests/test_sap/test_tree_util.py +++ b/tests/test_sap/test_tree_util.py @@ -5,6 +5,7 @@ from itk_dev_shared_components.sap import tree_util, sap_login, multi_session + class TestTreeUtil(unittest.TestCase): """Tests relating to the module SAP.tree_util.""" @classmethod @@ -16,9 +17,8 @@ def setUpClass(cls): def tearDownClass(cls): sap_login.kill_sap() - def test_get_node_key_by_text(self): - """Test get_node_key_by_test. + """Test get_node_key_by_test. Test that strict search and fuzzy search works and throws errors on nonsense input. """ @@ -37,9 +37,8 @@ def test_get_node_key_by_text(self): 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 get_item_by_text. Test that strict search and fuzzy search works and throws errors on nonsense input. """ @@ -58,7 +57,6 @@ 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. @@ -88,5 +86,6 @@ def navigate_to_test_page(): session.findById("wnd[0]/usr/ctxtGPART_DYN").text = "25564617" session.findById("wnd[0]").sendVKey(0) + if __name__ == '__main__': unittest.main()