Skip to content

Commit 10ae378

Browse files
authored
Merge pull request #23 from itk-dev-rpa/release-1.1.0
Release 1.1.0
2 parents 61535b4 + b187d98 commit 10ae378

21 files changed

+256
-86
lines changed

.github/workflows/Changelog.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: Check Changelog
2+
3+
on: pull_request
4+
5+
jobs:
6+
changelog:
7+
runs-on: ubuntu-latest
8+
name: Changelog should be updated
9+
strategy:
10+
fail-fast: false
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v2
14+
with:
15+
fetch-depth: 2
16+
17+
- name: Git fetch
18+
run: git fetch
19+
20+
- name: Check that changelog has been updated.
21+
run: git diff --exit-code origin/${{ github.base_ref }} -- changelog.md && exit 1 || exit 0
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Pylint
1+
name: Linting
22

33
on: [push]
44

@@ -8,17 +8,25 @@ jobs:
88
strategy:
99
matrix:
1010
python-version: ["3.11"]
11+
fail-fast: false
1112
steps:
1213
- uses: actions/checkout@v3
1314
- name: Set up Python ${{ matrix.python-version }}
1415
uses: actions/setup-python@v3
1516
with:
1617
python-version: ${{ matrix.python-version }}
18+
1719
- name: Install dependencies
1820
run: |
1921
python -m pip install --upgrade pip
2022
pip install pylint
23+
pip install flake8
2124
pip install .
22-
- name: Analysing the code with pylint
25+
26+
- name: Analyzing the code with pylint
2327
run: |
2428
pylint --rcfile=.pylintrc $(git ls-files '*.py')
29+
30+
- name: Analyzing the code with flake8
31+
run: |
32+
flake8 --extend-ignore=E501,E251 $(git ls-files '*.py')

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ celerybeat.pid
123123

124124
# Environments
125125
.env
126-
.venv
126+
.venv*
127127
env/
128128
venv/
129129
ENV/

README.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
# itk-dev-shared-components
22

3-
https://pypi.org/project/ITK-dev-shared-components/
3+
## Links
44

5-
pip install ITK-dev-shared-components
5+
[Documentation](https://itk-dev-rpa.github.io/itk-dev-shared-components-docs/)
6+
7+
[Pypi](https://pypi.org/project/ITK-dev-shared-components/)
8+
9+
## Installation
10+
11+
```
12+
pip install itk-dev-shared-components
13+
```
14+
15+
## Intro
16+
17+
This python library contains helper modules for RPA development.
18+
It's based on the need of [ITK Dev](https://itk.aarhus.dk/), but it has been
19+
generalized to be useful for others as well.
20+
21+
## Integrations
22+
23+
### SAP Gui
24+
25+
Helper functions for using SAP Gui. A few examples include:
26+
27+
- Login to SAP.
28+
- Handling multiple sessions in multiple threads.
29+
- Convenience functions for gridviews and trees.
30+
31+
### Microsoft Graph
32+
33+
Helper functions for using Microsoft Graph to read emails from shared inboxes.
34+
Some examples are:
35+
36+
- Authentication using username and password.
37+
- List and get emails.
38+
- Get attachment data.
39+
- Move and delete emails.

changelog.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [1.1.0] - 2023-11-28
11+
12+
### Changed
13+
14+
- sap.opret_kundekontakt: Function 'opret_kundekontakter' made more stable.
15+
- sap.multi_session: Function 'spawn_session' is no longer hardcoded to 1080p screen size.
16+
- sap.multi_session: Arrange session windows moved to separate function.
17+
- Bunch o' linting.
18+
- run_tests.bat: Dedicated test venv.
19+
- readme: Updated readme.
20+
21+
### Added
22+
23+
- Changelog!
24+
- pylint.yml: Flake8 added.
25+
26+
### Fixed
27+
28+
- sap.multi_session: Critical bug in 'run_batches'.
29+
- tests.sap.login: Change environ 'SAP Login' for later tests.
30+
31+
## [1.0.0] - 2023-11-14
32+
33+
- Initial release
34+
35+
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/1.1.0...HEAD
36+
[1.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.1.0
37+
[1.0.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/1.0.0

itk_dev_shared_components/graph/authentication.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import msal
55

6+
67
# pylint: disable-next=too-few-public-methods
78
class GraphAccess:
89
"""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
4546
password: The password of the user.
4647
client_id: The Graph API client id in 8-4-4-12 format.
4748
tenant_id: The Graph API tenant id in 8-4-4-12 format.
48-
49+
4950
Returns:
5051
GraphAccess: The GraphAccess object used to authorize Graph access.
5152
"""

itk_dev_shared_components/graph/mail.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,15 @@ def get_attachment_data(attachment: Attachment, graph_access: GraphAccess) -> io
162162
"""
163163
email = attachment.email
164164
endpoint = f"https://graph.microsoft.com/v1.0/users/{email.user}/messages/{email.id}/attachments/{attachment.id}/$value"
165-
response = _get_request(endpoint, graph_access)
165+
response = _get_request(endpoint, graph_access)
166166
data_bytes = response.content
167167
return io.BytesIO(data_bytes)
168168

169169

170-
def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, well_known_folder: bool=False) -> None:
170+
def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, well_known_folder: bool = False) -> None:
171171
"""Move an email to another folder under the same user.
172172
If well_known_folder is true, the folder path is assumed to be a well defined folder.
173-
See https://learn.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0
173+
See https://learn.microsoft.com/en-us/graph/api/resources/mailfolder?view=graph-rest-1.0
174174
for a list of well defined folder names.
175175
176176
Args:
@@ -208,7 +208,7 @@ def move_email(email: Email, folder_path: str, graph_access: GraphAccess, *, wel
208208
email.id = new_id
209209

210210

211-
def delete_email(email: Email, graph_access: GraphAccess, *, permanent: bool=False) -> None:
211+
def delete_email(email: Email, graph_access: GraphAccess, *, permanent: bool = False) -> None:
212212
"""Delete an email from the mailbox.
213213
If permanent is true the email is completely removed from the user's mailbox.
214214
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
235235

236236

237237
def _find_folder(response: dict, target_folder: str) -> str:
238-
"""Find the target folder in
238+
"""Find the target folder in
239239
240240
Args:
241241
response: The json dict of the HTTP response.
@@ -300,7 +300,7 @@ def _get_request(endpoint: str, graph_access: GraphAccess) -> requests.models.Re
300300
301301
Returns:
302302
Response: The response object of the GET request.
303-
303+
304304
Raises:
305305
HTTPError: Any errors raised while performing GET request.
306306
"""

itk_dev_shared_components/sap/gridview_util.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""This module provides static functions to perform common tasks with SAP GuiGridView COM objects."""
22

3+
34
def scroll_entire_table(grid_view, return_to_top=False) -> None:
45
"""This function scrolls through the entire table to load all cells.
56
@@ -23,7 +24,7 @@ def get_all_rows(grid_view, pre_load=True) -> tuple[tuple[str]]:
2324
Args:
2425
grid_view: A SAP GuiGridView object.
2526
pre_load: Whether to first scroll through the table to load all values.
26-
If a row hasn't been loaded before reading, the row data will be empty.
27+
If a row hasn't been loaded before reading, the row data will be empty.
2728
2829
Returns:
2930
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]]:
4849
return tuple(output)
4950

5051

51-
def get_row(grid_view, row:int, scroll_to_row=False) -> tuple[str]:
52+
def get_row(grid_view, row: int, scroll_to_row=False) -> tuple[str]:
5253
"""Returns the data of a single row.
5354
5455
Args:
5556
grid_view: A SAP GuiGridView object.
5657
row: The zero-based index of the row.
5758
scroll_to_row: Whether to scroll to the row before reading the data.
58-
This ensures the data of the row has been loaded before reading.
59+
This ensures the data of the row has been loaded before reading.
5960
6061
Returns:
6162
tuple[str]: A tuple of the row's data.
@@ -108,7 +109,7 @@ def get_column_titles(grid_view) -> tuple[str]:
108109
return tuple(grid_view.GetColumnTitles(c)[0] for c in grid_view.ColumnOrder)
109110

110111

111-
def find_row_index_by_value(grid_view, column:str, value:str) -> int:
112+
def find_row_index_by_value(grid_view, column: str, value: str) -> int:
112113
"""Find the index of the first row where the given column's value
113114
matches the given value. Returns -1 if no row is found.
114115
@@ -137,7 +138,8 @@ def find_row_index_by_value(grid_view, column:str, value:str) -> int:
137138

138139
return -1
139140

140-
def find_all_row_indices_by_value(grid_view, column:str, value:str) -> list[int]:
141+
142+
def find_all_row_indices_by_value(grid_view, column: str, value: str) -> list[int]:
141143
"""Find all indices of the rows where the given column's value
142144
match the given value. Returns an empty list if no row is found.
143145

itk_dev_shared_components/sap/multi_session.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@
1010
import pythoncom
1111
import win32com.client
1212
import win32gui
13+
import win32api
1314

14-
def run_with_session(session_index:int, func:Callable, args:tuple) -> None:
15+
16+
def run_with_session(session_index: int, func: Callable, args: tuple) -> None:
1517
"""Run a function in a specific session based on the sessions index.
1618
This function is meant to be run inside a separate thread.
1719
The function must take a session object as its first argument.
1820
Note that this function will not spawn the sessions before running,
1921
use spawn_sessions to do that.
2022
"""
21-
2223
pythoncom.CoInitialize()
2324

2425
sap = win32com.client.GetObject("SAPGUI")
@@ -31,17 +32,24 @@ def run_with_session(session_index:int, func:Callable, args:tuple) -> None:
3132
pythoncom.CoUninitialize()
3233

3334

34-
def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None:
35+
def run_batch(func: Callable, args: tuple[tuple]) -> None:
3536
"""Run a function in parallel sessions.
36-
The function will be run {num_sessions} times with args[i] as input.
37+
A number of threads equal to the length of args will be spawned.
3738
The function must take a session object as its first argument.
3839
Note that this function will not spawn the sessions before running,
3940
use spawn_sessions to do that.
41+
42+
Args:
43+
func: A callable function to run in the threads.
44+
args: A tuple of tuples containing arguments to be passed to func.
45+
46+
Raises:
47+
Exception: Any exception raised in any of the threads.
4048
"""
4149

4250
threads = []
43-
for i in range(num_sessions):
44-
t = ExThread(target=run_with_session, args=(i, func, args[i]))
51+
for i, arg in enumerate(args):
52+
t = ExThread(target=run_with_session, args=(i, func, arg))
4553
threads.append(t)
4654

4755
for t in threads:
@@ -53,7 +61,7 @@ def run_batch(func:Callable, args:tuple[tuple], num_sessions=6) -> None:
5361
raise t.error
5462

5563

56-
def run_batches(func:Callable, args:tuple[tuple], num_sessions=6):
64+
def run_batches(func: Callable, args: tuple[tuple], num_sessions: int = 6) -> None:
5765
"""Run a function in parallel batches.
5866
This function runs the input function for each set of arguments in args.
5967
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):
6472

6573
for b in range(0, len(args), num_sessions):
6674
batch = args[b:b+num_sessions]
67-
run_batch(func, args, len(batch))
75+
run_batch(func, batch)
6876

6977

7078
def spawn_sessions(num_sessions=6) -> list:
@@ -100,14 +108,39 @@ def spawn_sessions(num_sessions=6) -> list:
100108
while connection.Sessions.count < num_sessions:
101109
time.sleep(0.1)
102110

103-
sessions = tuple(connection.Sessions)
111+
arrange_sessions()
112+
113+
return tuple(connection.Sessions)
114+
115+
116+
def get_all_sap_sessions() -> tuple:
117+
"""Returns a tuple of all open SAP sessions (on connection index 0).
118+
119+
Returns:
120+
tuple: A tuple of SAP GuiSession objects.
121+
"""
122+
sap = win32com.client.GetObject("SAPGUI")
123+
app = sap.GetScriptingEngine
124+
connection = app.Connections(0)
125+
return tuple(connection.Sessions)
126+
127+
128+
def arrange_sessions():
129+
"""Take all toplevel windows of currently open SAP sessions
130+
and arrange them equally on the screen.
131+
"""
132+
sessions = get_all_sap_sessions()
104133
num_sessions = len(sessions)
105134

106135
# Calculate number of columns and rows
107136
c = math.ceil(math.sqrt(num_sessions))
108137
r = math.ceil(num_sessions / c)
109138

110-
w, h = 1920//c, 1040//r
139+
screen_width = win32api.GetSystemMetrics(0)
140+
screen_height = win32api.GetSystemMetrics(1)
141+
142+
w = screen_width // c
143+
h = screen_height // r
111144

112145
for i, session in enumerate(sessions):
113146
window = session.findById("wnd[0]")
@@ -117,20 +150,6 @@ def spawn_sessions(num_sessions=6) -> list:
117150
y = i // c * h
118151
win32gui.MoveWindow(hwnd, x, y, w, h, True)
119152

120-
return sessions
121-
122-
123-
def get_all_sap_sessions() -> tuple:
124-
"""Returns a tuple of all open SAP sessions (on connection index 0).
125-
126-
Returns:
127-
tuple: A tuple of SAP GuiSession objects.
128-
"""
129-
sap = win32com.client.GetObject("SAPGUI")
130-
app = sap.GetScriptingEngine
131-
connection = app.Connections(0)
132-
return tuple(connection.Sessions)
133-
134153

135154
class ExThread(threading.Thread):
136155
"""A thread with a handle to get an exception raised inside the thread: ExThread.error"""
@@ -141,5 +160,5 @@ def __init__(self, *args, **kwargs):
141160
def run(self):
142161
try:
143162
self._target(*self._args, **self._kwargs)
144-
except Exception as e: # pylint: disable=broad-exception-caught
163+
except Exception as e: # pylint: disable=broad-exception-caught
145164
self.error = e

0 commit comments

Comments
 (0)