Skip to content

Commit 80abd2e

Browse files
authored
Automated ABR Calibration Data Uploading (#14782)
<!-- Thanks for taking the time to open a pull request! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide: https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability! --> # Overview Pulls Calibration Data from Robots and Uploads to google_drive/google_sheet # Test Plan Tested on ABR robots. Successfully pulls calibration data, uploads to google drive, and saves to google sheet. # Changelog - Adds abr_calibration_logs.py 1. Connects to google drive folder 2. Connects to google sheet 3. Pulls module, instrument, and deck calibration data and compiles into one .json file per robot via http requests 4. Uploads new files to google drive folder 5. adds new rows to instrument, module, and deck calibration sheets if the serial and calibration lastmodified timestamp pairing do not already exist - Split jira_tool up into a file with just jira_tools and a file that uses the tools with the robots. - For all scripts uploading to google drive, changed the folder_name argument to folder_id so that the service_account is writing to the correct folder. Adds email as argument to allow for permission sharing by service account. # Review requests <!-- Describe any requests for your reviewers here. --> # Risk assessment <!-- Carefully go over your pull request and look at the other parts of the codebase it may affect. Look for the possibility, even if you think it's small, that your change may affect some other part of the system - for instance, changing return tip behavior in protocol may also change the behavior of labware calibration. Identify the other parts of the system your codebase may affect, so that in addition to your own review and testing, other people who may not have the system internalized as much as you can focus their attention and testing there. -->
1 parent 6ccb243 commit 80abd2e

File tree

8 files changed

+514
-165
lines changed

8 files changed

+514
-165
lines changed

abr-testing/abr_testing/automation/google_drive_tool.py

+51-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Google Drive Tool."""
22
import os
3-
from typing import Set, Any
3+
from typing import Set, Any, Optional
4+
import webbrowser
5+
import mimetypes
46
from oauth2client.service_account import ServiceAccountCredentials # type: ignore[import]
57
from googleapiclient.discovery import build
68
from googleapiclient.http import MediaFileUpload
@@ -14,15 +16,16 @@
1416
class google_drive:
1517
"""Google Drive Tool."""
1618

17-
def __init__(self, credentials: Any, folder_name: str, parent_folder: Any) -> None:
19+
def __init__(self, credentials: Any, folder_name: str, email: str) -> None:
1820
"""Connects to google drive via credentials file."""
1921
self.scope = ["https://www.googleapis.com/auth/drive"]
2022
self.credentials = ServiceAccountCredentials.from_json_keyfile_name(
2123
credentials, self.scope
2224
)
2325
self.drive_service = build("drive", "v3", credentials=self.credentials)
24-
self.folder_name = folder_name
25-
self.parent_folder = parent_folder
26+
self.parent_folder = folder_name
27+
self.email = email
28+
self.folder = self.open_folder()
2629

2730
def list_folder(self, delete: Any = False) -> Set[str]:
2831
"""List folders and files in Google Drive."""
@@ -72,26 +75,37 @@ def upload_file(self, file_path: str) -> str:
7275
"""Upload file to Google Drive."""
7376
file_metadata = {
7477
"name": os.path.basename(file_path),
75-
"mimeType": "application/vnd.google-apps.folder",
76-
"parents": [self.parent_folder] if self.parent_folder else "",
78+
"mimeType": str(mimetypes.guess_type(file_path)[0]),
79+
"parents": [self.parent_folder],
7780
}
78-
7981
media = MediaFileUpload(file_path, resumable=True)
8082

8183
uploaded_file = (
8284
self.drive_service.files()
8385
.create(body=file_metadata, media_body=media, fields="id") # type: ignore
8486
.execute()
8587
)
86-
8788
return uploaded_file["id"]
8889

89-
def upload_missing_files(self, storage_directory: str, missing_files: set) -> None:
90+
def upload_missing_files(self, storage_directory: str) -> None:
9091
"""Upload missing files to Google Drive."""
92+
# Read Google Drive .json files.
93+
google_drive_files = self.list_folder()
94+
google_drive_files_json = [
95+
file for file in google_drive_files if file.endswith(".json")
96+
]
97+
# Read local directory.
98+
local_files_json = set(
99+
file for file in os.listdir(storage_directory) if file.endswith(".json")
100+
)
101+
missing_files = local_files_json - set(google_drive_files_json)
102+
print(f"Missing files: {len(missing_files)}")
103+
# Upload missing files.
91104
uploaded_files = []
92105
for file in missing_files:
93106
file_path = os.path.join(storage_directory, file)
94107
uploaded_file_id = google_drive.upload_file(self, file_path)
108+
self.share_permissions(uploaded_file_id)
95109
uploaded_files.append(
96110
{"name": os.path.basename(file_path), "id": uploaded_file_id}
97111
)
@@ -108,3 +122,31 @@ def upload_missing_files(self, storage_directory: str, missing_files: set) -> No
108122
print(
109123
f"File '{this_name}' was not found in the list of files after uploading."
110124
)
125+
126+
def open_folder(self) -> Optional[str]:
127+
"""Open folder in web browser."""
128+
folder_metadata = (
129+
self.drive_service.files()
130+
.get(fileId=self.parent_folder, fields="webViewLink")
131+
.execute()
132+
)
133+
folder_link = folder_metadata.get("webViewLink")
134+
if folder_link:
135+
print(f"Folder link: {folder_link}")
136+
webbrowser.open(
137+
folder_link
138+
) # Open the folder link in the default web browser
139+
else:
140+
print("Folder link not found.")
141+
return folder_link
142+
143+
def share_permissions(self, file_id: str) -> None:
144+
"""Share permissions with self."""
145+
new_permission = {
146+
"type": "user",
147+
"role": "writer",
148+
"emailAddress": self.email,
149+
}
150+
self.drive_service.permissions().create(
151+
fileId=file_id, body=new_permission, transferOwnership=False # type: ignore
152+
).execute()

abr-testing/abr_testing/automation/jira_tool.py

-114
Original file line numberDiff line numberDiff line change
@@ -6,77 +6,6 @@
66
import webbrowser
77
import argparse
88
from typing import List, Tuple
9-
from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs
10-
11-
12-
def get_error_runs_from_robot(ip: str) -> List[str]:
13-
"""Get runs that have errors from robot."""
14-
error_run_ids = []
15-
response = requests.get(
16-
f"http://{ip}:31950/runs", headers={"opentrons-version": "3"}
17-
)
18-
run_data = response.json()
19-
run_list = run_data["data"]
20-
for run in run_list:
21-
run_id = run["id"]
22-
num_of_errors = len(run["errors"])
23-
if not run["current"] and num_of_errors > 0:
24-
error_run_ids.append(run_id)
25-
return error_run_ids
26-
27-
28-
def get_error_info_from_robot(
29-
ip: str, one_run: str, storage_directory: str
30-
) -> Tuple[str, str, str, List[str], str, str]:
31-
"""Get error information from robot to fill out ticket."""
32-
description = dict()
33-
# get run information
34-
results = get_run_logs.get_run_data(one_run, ip)
35-
# save run information to local directory as .json file
36-
saved_file_path = read_robot_logs.save_run_log_to_json(
37-
ip, results, storage_directory
38-
)
39-
40-
# Error Printout
41-
(
42-
num_of_errors,
43-
error_type,
44-
error_code,
45-
error_instrument,
46-
error_level,
47-
) = read_robot_logs.get_error_info(results)
48-
# JIRA Ticket Fields
49-
failure_level = "Level " + str(error_level) + " Failure"
50-
components = [failure_level, "Flex-RABR"]
51-
affects_version = results["API_Version"]
52-
parent = results.get("robot_name", "")
53-
print(parent)
54-
summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type
55-
# Description of error
56-
description["protocol_name"] = results["protocol"]["metadata"].get(
57-
"protocolName", ""
58-
)
59-
description["error"] = " ".join([error_code, error_type, error_instrument])
60-
description["protocol_step"] = list(results["commands"])[-1]
61-
description["right_mount"] = results.get("right", "No attachment")
62-
description["left_mount"] = results.get("left", "No attachment")
63-
description["gripper"] = results.get("extension", "No attachment")
64-
all_modules = abr_google_drive.get_modules(results)
65-
whole_description = {**description, **all_modules}
66-
whole_description_str = (
67-
"{"
68-
+ "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items())
69-
+ "}"
70-
)
71-
72-
return (
73-
summary,
74-
parent,
75-
affects_version,
76-
components,
77-
whole_description_str,
78-
saved_file_path,
79-
)
809

8110

8211
class JiraTicket:
@@ -193,20 +122,6 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None
193122
if __name__ == "__main__":
194123
"""Create ticket for specified robot."""
195124
parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.")
196-
parser.add_argument(
197-
"storage_directory",
198-
metavar="STORAGE_DIRECTORY",
199-
type=str,
200-
nargs=1,
201-
help="Path to long term storage directory for run logs.",
202-
)
203-
parser.add_argument(
204-
"robot_ip",
205-
metavar="ROBOT_IP",
206-
type=str,
207-
nargs=1,
208-
help="IP address of robot as string.",
209-
)
210125
parser.add_argument(
211126
"jira_api_token",
212127
metavar="JIRA_API_TOKEN",
@@ -238,38 +153,9 @@ def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None
238153
help="JIRA Board ID. RABR is 217",
239154
)
240155
args = parser.parse_args()
241-
storage_directory = args.storage_directory[0]
242-
ip = args.robot_ip[0]
243156
url = "https://opentrons.atlassian.net"
244157
api_token = args.jira_api_token[0]
245158
email = args.email[0]
246159
board_id = args.board_id[0]
247160
reporter_id = args.reporter_id[0]
248161
ticket = JiraTicket(url, api_token, email)
249-
error_runs = get_error_runs_from_robot(ip)
250-
one_run = error_runs[-1] # Most recent run with error.
251-
(
252-
summary,
253-
robot,
254-
affects_version,
255-
components,
256-
whole_description_str,
257-
saved_file_path,
258-
) = get_error_info_from_robot(ip, one_run, storage_directory)
259-
print(f"Making ticket for run: {one_run} on robot {robot}.")
260-
# TODO: make argument or see if I can get rid of with using board_id.
261-
project_key = "RABR"
262-
parent_key = project_key + "-" + robot[-1]
263-
issue_url, issue_key = ticket.create_ticket(
264-
summary,
265-
whole_description_str,
266-
project_key,
267-
reporter_id,
268-
"Bug",
269-
"Medium",
270-
components,
271-
affects_version,
272-
parent_key,
273-
)
274-
ticket.open_issue(issue_key)
275-
ticket.post_attachment_to_ticket(issue_key, saved_file_path)

0 commit comments

Comments
 (0)