Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 1 | # |
| 2 | # TestRail API binding for Python 3.x (API v2, available since |
| 3 | # TestRail 3.0) |
| 4 | # Compatible with TestRail 3.0 and later. |
| 5 | # |
| 6 | # Learn more: |
| 7 | # |
| 8 | # http://docs.gurock.com/testrail-api2/start |
| 9 | # http://docs.gurock.com/testrail-api2/accessing |
| 10 | # |
| 11 | # Copyright Gurock Software GmbH. See license.md for details. |
| 12 | # |
| 13 | |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 14 | import base64 |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 15 | import json |
| 16 | |
| 17 | import requests |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 18 | |
| 19 | |
| 20 | class APIClient: |
| 21 | def __init__(self, base_url): |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 22 | self.user = "" |
| 23 | self.password = "" |
| 24 | if not base_url.endswith("/"): |
| 25 | base_url += "/" |
| 26 | self.__url = base_url + "index.php?/api/v2/" |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 27 | |
| 28 | # |
| 29 | # Send Get |
| 30 | # |
| 31 | # Issues a GET request (read) against the API and returns the result |
| 32 | # (as Python dict) or filepath if successful file download |
| 33 | # |
| 34 | # Arguments: |
| 35 | # |
| 36 | # uri The API method to call including parameters |
| 37 | # (e.g. get_case/1) |
| 38 | # |
| 39 | # filepath The path and file name for attachment download |
| 40 | # Used only for 'get_attachment/:attachment_id' |
| 41 | # |
| 42 | def send_get(self, uri, filepath=None): |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 43 | return self.__send_request("GET", uri, filepath) |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 44 | |
| 45 | # |
| 46 | # Send POST |
| 47 | # |
| 48 | # Issues a POST request (write) against the API and returns the result |
| 49 | # (as Python dict). |
| 50 | # |
| 51 | # Arguments: |
| 52 | # |
| 53 | # uri The API method to call including parameters |
| 54 | # (e.g. add_case/1) |
| 55 | # data The data to submit as part of the request (as |
| 56 | # Python dict, strings must be UTF-8 encoded) |
| 57 | # If adding an attachment, must be the path |
| 58 | # to the file |
| 59 | # |
| 60 | def send_post(self, uri, data): |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 61 | return self.__send_request("POST", uri, data) |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 62 | |
| 63 | def __send_request(self, method, uri, data): |
| 64 | url = self.__url + uri |
| 65 | |
| 66 | auth = str( |
| 67 | base64.b64encode( |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 68 | bytes("%s:%s" % (self.user, self.password), "utf-8") |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 69 | ), |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 70 | "ascii", |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 71 | ).strip() |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 72 | headers = {"Authorization": "Basic " + auth} |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 73 | |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 74 | if method == "POST": |
| 75 | if uri[:14] == "add_attachment": # add_attachment API method |
| 76 | files = {"attachment": (open(data, "rb"))} |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 77 | response = requests.post(url, headers=headers, files=files) |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 78 | files["attachment"].close() |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 79 | else: |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 80 | headers["Content-Type"] = "application/json" |
| 81 | payload = bytes(json.dumps(data), "utf-8") |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 82 | response = requests.post(url, headers=headers, data=payload) |
| 83 | else: |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 84 | headers["Content-Type"] = "application/json" |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 85 | response = requests.get(url, headers=headers) |
| 86 | |
| 87 | if response.status_code > 201: |
| 88 | try: |
| 89 | error = response.json() |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 90 | except Exception: # response.content not formatted as JSON |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 91 | error = str(response.content) |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 92 | raise APIError( |
| 93 | "TestRail API returned HTTP %s (%s)" |
| 94 | % (response.status_code, error) |
| 95 | ) |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 96 | else: |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 97 | if uri[:15] == "get_attachment/": # Expecting file, not JSON |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 98 | try: |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 99 | open(data, "wb").write(response.content) |
| 100 | return data |
| 101 | except Exception: |
| 102 | return "Error saving attachment." |
Ilya Menkov | ad23403 | 2019-10-22 13:43:54 +0400 | [diff] [blame] | 103 | else: |
| 104 | return response.json() |
| 105 | |
| 106 | |
| 107 | class APIError(Exception): |
stavrovska | 28772bc | 2024-05-22 09:33:50 +0200 | [diff] [blame^] | 108 | pass |