Skip to content

Commit 6303442

Browse files
authored
Merge pull request #52 from itk-dev-rpa/release/2.3.0
Release/2.3.0
2 parents 8048dec + 8cae6a6 commit 6303442

File tree

10 files changed

+372
-84
lines changed

10 files changed

+372
-84
lines changed

.github/workflows/Linting.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Linting
22

3-
on: [push]
3+
on: [push, pull_request]
44

55
jobs:
66
build:
@@ -29,4 +29,4 @@ jobs:
2929
3030
- name: Analyzing the code with flake8
3131
run: |
32-
flake8 --extend-ignore=E501,E251 $(git ls-files '*.py')
32+
flake8 --extend-ignore=E501,E251 $(git ls-files '*.py')

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,9 @@ cython_debug/
160160
# and can be added to the global gitignore or merged into this file. For a more nuclear
161161
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
162162
#.idea/
163+
164+
# Ruff config file
165+
ruff.toml
166+
167+
# Pre-commit YAML file
168+
.pre-commit-config.yaml

changelog.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.3.0] - 2024-07-03
11+
12+
### Added
13+
14+
- Function for getting cases based on CVR from KMD Nova.
15+
- Tests for getting cases based on CVR from KMD Nova.
16+
- Module for accessing site and file endpoints in Microsoft Graph.
17+
18+
### Changed
19+
20+
- Unexpected format on caseworker in Nova cases results in None.
21+
- Minor refactoring to move common HTTP request wrappers for Microsoft Graph into their own file.
22+
1023
## [2.2.0] - 2024-05-08
1124

1225
### Added
@@ -101,7 +114,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
101114

102115
- Initial release
103116

104-
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.2.0...HEAD
117+
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.3.0...HEAD
118+
[2.3.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.3.0
105119
[2.2.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.2.0
106120
[2.1.1] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.1
107121
[2.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.0
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""This module contains common functions like HTTP request wrappers which are used across other modules."""
2+
3+
from typing import Any
4+
5+
import requests
6+
7+
from itk_dev_shared_components.graph.authentication import GraphAccess
8+
9+
10+
def get_request(endpoint: str, graph_access: GraphAccess) -> requests.models.Response:
11+
"""Sends a get request to the given Graph endpoint using the GraphAccess
12+
and returns the json object of the response.
13+
14+
Args:
15+
endpoint: The URL of the Graph endpoint.
16+
graph_access: The GraphAccess object used to authenticate.
17+
18+
Returns:
19+
Response: The response object of the GET request.
20+
21+
Raises:
22+
HTTPError: Any errors raised while performing GET request.
23+
"""
24+
token = graph_access.get_access_token()
25+
headers = {"Authorization": f"Bearer {token}"}
26+
27+
response = requests.get(endpoint, headers=headers, timeout=30)
28+
response.raise_for_status()
29+
30+
return response
31+
32+
33+
def put_request(endpoint: str, graph_access: GraphAccess, data: Any) -> requests.models.Response:
34+
"""Sends a put request to the given Graph endpoint using the GraphAccess
35+
and returns the json object of the response.
36+
37+
Args:
38+
endpoint: The URL of the Graph endpoint.
39+
data: The data to send in the request.
40+
graph_access: The GraphAccess object used to authenticate.
41+
42+
Returns:
43+
Response: The response object of the PUT request.
44+
45+
Raises:
46+
HTTPError: Any errors raised while performing PUT request.
47+
"""
48+
token = graph_access.get_access_token()
49+
headers = {"Authorization": f"Bearer {token}"}
50+
51+
response = requests.put(endpoint, headers=headers, data=data, timeout=30)
52+
response.raise_for_status()
53+
54+
return response
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""This module is responsible for accessing files using the Microsoft Graph API."""
2+
3+
from dataclasses import dataclass, field
4+
5+
from itk_dev_shared_components.graph.authentication import GraphAccess
6+
from itk_dev_shared_components.graph.common import get_request
7+
8+
9+
@dataclass
10+
class DriveItem:
11+
"""A class representing a DriveItem."""
12+
13+
id: str = field(repr=False)
14+
name: str
15+
web_url: str
16+
last_modified: str
17+
18+
19+
def get_drive_item(graph_access: GraphAccess, site_id: str, drive_item_path: str) -> str:
20+
"""Given a site id and a drive_item_path, gets the corresponding DriveItem
21+
22+
You need to authorize against Graph to get the GraphAccess before using this function
23+
see the graph.authentication module.
24+
25+
See https://learn.microsoft.com/en-us/graph/api/driveitem-get for further documentation
26+
27+
Args:
28+
graph_access: The GraphAccess object used to authenticate.
29+
site_id: The id of the site in SharePoint.
30+
drive_item_path: The path to the DriveItem in SharePoint.
31+
"""
32+
endpoint = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root:/{drive_item_path}"
33+
response = get_request(endpoint, graph_access)
34+
raw_response = response.json()
35+
36+
return _unpack_drive_item_response(raw_response)
37+
38+
39+
def _unpack_drive_item_response(drive_item_raw: dict[str, str]) -> DriveItem:
40+
"""Unpack a json HTTP response and create a DriveItem object.
41+
42+
Args:
43+
drive_item_raw: The json dictionary created by response.json().
44+
45+
Returns:
46+
DriveItem: A DriveItem object.
47+
"""
48+
49+
return DriveItem(
50+
id=drive_item_raw["id"],
51+
name=drive_item_raw["name"],
52+
web_url=drive_item_raw["webUrl"],
53+
last_modified=drive_item_raw["lastModifiedDateTime"],
54+
)

itk_dev_shared_components/graph/mail.py

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from bs4 import BeautifulSoup
88

99
from itk_dev_shared_components.graph.authentication import GraphAccess
10+
from itk_dev_shared_components.graph.common import get_request
1011

1112

1213
@dataclass
@@ -67,7 +68,7 @@ def get_emails_from_folder(user: str, folder_path: str, graph_access: GraphAcces
6768

6869
endpoint = f"https://graph.microsoft.com/v1.0/users/{user}/mailFolders/{folder_id}/messages?$top=1000"
6970

70-
response = _get_request(endpoint, graph_access)
71+
response = get_request(endpoint, graph_access)
7172
emails_raw = response.json()['value']
7273

7374
return _unpack_email_response(user, emails_raw)
@@ -84,7 +85,7 @@ def get_email_as_mime(email: Email, graph_access: GraphAccess) -> io.BytesIO:
8485
io.BytesIO: A file-like object of the MIME file.
8586
"""
8687
endpoint = f"https://graph.microsoft.com/v1.0/users/{email.user}/messages/{email.id}/$value"
87-
response = _get_request(endpoint, graph_access)
88+
response = get_request(endpoint, graph_access)
8889
data = response.content
8990
return io.BytesIO(data)
9091

@@ -113,15 +114,15 @@ def get_folder_id_from_path(user: str, folder_path: str, graph_access: GraphAcce
113114

114115
# Get main folder
115116
endpoint = f"https://graph.microsoft.com/v1.0/users/{user}/mailFolders"
116-
response = _get_request(endpoint, graph_access).json()
117+
response = get_request(endpoint, graph_access).json()
117118
folder_id = _find_folder(response, main_folder)
118119
if folder_id is None:
119120
raise ValueError(f"Top level folder '{main_folder}' was not found for user '{user}'.")
120121

121122
# Get child folders
122123
for child_folder in child_folders:
123124
endpoint = f"https://graph.microsoft.com/v1.0/users/{user}/mailFolders/{folder_id}/childFolders"
124-
response = _get_request(endpoint, graph_access).json()
125+
response = get_request(endpoint, graph_access).json()
125126
folder_id = _find_folder(response, child_folder)
126127
if folder_id is None:
127128
raise ValueError(f"Child folder '{child_folder}' not found under '{main_folder}' for user '{user}'.")
@@ -141,7 +142,7 @@ def list_email_attachments(email: Email, graph_access: GraphAccess) -> tuple[Att
141142
tuple[Attachment]: A tuple of Attachment objects describing the attachments.
142143
"""
143144
endpoint = f"https://graph.microsoft.com/v1.0/users/{email.user}/messages/{email.id}/attachments?$select=name,size,id"
144-
response = _get_request(endpoint, graph_access).json()
145+
response = get_request(endpoint, graph_access).json()
145146

146147
attachments = []
147148
for att in response['value']:
@@ -162,7 +163,7 @@ def get_attachment_data(attachment: Attachment, graph_access: GraphAccess) -> io
162163
"""
163164
email = attachment.email
164165
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)
166+
response = get_request(endpoint, graph_access)
166167
data_bytes = response.content
167168
return io.BytesIO(data_bytes)
168169

@@ -287,31 +288,3 @@ def _unpack_email_response(user: str, emails_raw: list[dict[str, str]]) -> tuple
287288
)
288289

289290
return tuple(emails)
290-
291-
292-
def _get_request(endpoint: str, graph_access: GraphAccess) -> requests.models.Response:
293-
"""Sends a get request to the given Graph endpoint using the GraphAccess
294-
and returns the json object of the response.
295-
296-
Args:
297-
endpoint: The URL of the Graph endpoint.
298-
graph_access: The GraphAccess object used to authenticate.
299-
timeout: Timeout in seconds of the HTTP request. Defaults to 10.
300-
301-
Returns:
302-
Response: The response object of the GET request.
303-
304-
Raises:
305-
HTTPError: Any errors raised while performing GET request.
306-
"""
307-
token = graph_access.get_access_token()
308-
headers = {'Authorization': f"Bearer {token}"}
309-
310-
response = requests.get(
311-
endpoint,
312-
headers=headers,
313-
timeout=30
314-
)
315-
response.raise_for_status()
316-
317-
return response
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""This module is responsible for accessing sites using the Microsoft Graph API."""
2+
3+
from dataclasses import dataclass, field
4+
5+
from itk_dev_shared_components.graph.authentication import GraphAccess
6+
from itk_dev_shared_components.graph.common import get_request, put_request
7+
8+
9+
@dataclass
10+
class Site:
11+
"""A class representing a Site."""
12+
13+
id: str = field(repr=False)
14+
name: str
15+
display_name: str
16+
description: str
17+
web_url: str
18+
created: str
19+
last_modified: str
20+
21+
22+
def get_site(graph_access: GraphAccess, site_path: str) -> Site:
23+
"""Retrieve properties and relationships for a site resource.
24+
A site resource represents a team site in SharePoint.
25+
26+
You need to authorize against Graph to get the GraphAccess before using this function
27+
see the graph.authentication module.
28+
29+
See https://learn.microsoft.com/en-us/graph/api/site-get?view=graph-rest-1.0
30+
for a list of possible site_paths to pass as argument.
31+
32+
Args:
33+
graph_access: The GraphAccess object used to authenticate.
34+
site_path: The path to the team site in SharePoint.
35+
36+
returns:
37+
A Site object
38+
"""
39+
endpoint = f"https://graph.microsoft.com/v1.0/sites/{site_path}"
40+
response = get_request(endpoint, graph_access)
41+
42+
return _unpack_site_response(response.json())
43+
44+
45+
def download_file_contents(graph_access: GraphAccess, site_id: str, drive_item_id: str) -> bytes:
46+
"""Given a site_id, a drive_item_id, and a download destination, downloads a single file from a site resource
47+
48+
You need to authorize against Graph to get the GraphAccess before using this function
49+
see the graph.authentication module.
50+
51+
See https://learn.microsoft.com/en-us/graph/api/driveitem-get-content for further documentation
52+
53+
Args:
54+
graph_access: The GraphAccess object used to authenticate.
55+
site_id: The id of the site in SharePoint.
56+
drive_item_id: The id of the DriveItem in SharePoint.
57+
58+
returns:
59+
bytes containing the contents of the file
60+
"""
61+
endpoint = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/items/{drive_item_id}/content"
62+
response = get_request(endpoint, graph_access)
63+
return response.content
64+
65+
66+
def upload_file_contents(graph_access: GraphAccess, site_id: str, drive_item_path: str, file_contents: bytes):
67+
"""Given a site_id, a drive_item_path, and file_contents as bytes, uploads a single file to a site
68+
69+
You need to authorize against Graph to get the GraphAccess before using this function
70+
see the graph.authentication module.
71+
72+
See https://learn.microsoft.com/en-us/graph/api/driveitem-put-content for further documentation
73+
74+
Args:
75+
graph_access: The GraphAccess object used to authenticate.
76+
site_id: The id of the site in SharePoint.
77+
drive_item_path: The path to upload the file contents to.
78+
file_contents: A bytes object containing the file's contents.
79+
80+
"""
81+
82+
endpoint = f"https://graph.microsoft.com/v1.0/sites/{site_id}/drive/root:/{drive_item_path}:/content"
83+
put_request(endpoint, graph_access, file_contents)
84+
85+
86+
def _unpack_site_response(site_raw: dict[str, str]) -> Site:
87+
"""Unpack a json HTTP response and create a Site object.
88+
89+
Args:
90+
site_raw: The json dictionary created by response.json().
91+
92+
Returns:
93+
Site: A Site object.
94+
"""
95+
96+
return Site(
97+
id=site_raw["id"],
98+
name=site_raw["name"],
99+
display_name=site_raw["displayName"],
100+
description=site_raw["description"],
101+
web_url=site_raw["webUrl"],
102+
created=site_raw["createdDateTime"],
103+
last_modified=site_raw["lastModifiedDateTime"],
104+
)

0 commit comments

Comments
 (0)