blob: a1b45a970537987e7fd78d87c49483684cd57996 [file] [log] [blame]
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +04001from __future__ import absolute_import
stavrovska28772bc2024-05-22 09:33:50 +02002
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +04003import logging
4import time
5
6import requests
7
8logger = logging.getLogger(__name__)
9
stavrovska28772bc2024-05-22 09:33:50 +020010requests_logger = logging.getLogger("requests.packages.urllib3")
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040011requests_logger.setLevel(logging.WARNING)
12
13
14class ItemSet(list):
15 def __init__(self, *args, **kwargs):
16 self._item_class = None
17 return super(ItemSet, self).__init__(*args, **kwargs)
18
19 def find_all(self, **kwargs):
20 filtered = ItemSet(
stavrovska28772bc2024-05-22 09:33:50 +020021 x
22 for x in self
23 if all(getattr(x, k) == v for k, v in kwargs.items())
24 )
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040025 filtered._item_class = self._item_class
26 return filtered
27
28 def find(self, **kwargs):
29 items = self.find_all(**kwargs)
30 if items:
31 return items[0]
32 else:
33 raise NotFound(self._item_class, **kwargs)
34
35
36class Collection(object):
37
stavrovska28772bc2024-05-22 09:33:50 +020038 _list_url = "get_{name}s"
39 _add_url = "add_{name}"
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040040
41 def __init__(self, item_class=None, parent_id=None, **kwargs):
42 self._item_class = item_class
43 self._handler = self._item_class._handler
44 self.parent_id = parent_id
45 for k, v in kwargs.items():
46 setattr(self, k, v)
47
48 def __call__(self, id=None):
49 name = self._item_class._api_name()
50 if id is None:
51 items = self._list(name)
stavrovska28772bc2024-05-22 09:33:50 +020052 if "error" in items:
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040053 raise Exception(items)
stavrovska28772bc2024-05-22 09:33:50 +020054 if name == "project":
55 items = items["projects"]
56 if name == "case":
57 items = items["cases"]
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040058 items = ItemSet(self._to_object(x) for x in items)
59 items._item_class = self._item_class
60 return items
61
62 else:
63 return self._item_class.get(id)
64
65 def __repr__(self):
stavrovska28772bc2024-05-22 09:33:50 +020066 return "<Collection of {}>".format(self._item_class.__name__)
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040067
68 def _to_object(self, data):
69 return self._item_class(**data)
70
71 def _list(self, name, params=None):
72 params = params or {}
73 url = self._list_url.format(name=name)
74 if self.parent_id is not None:
stavrovska28772bc2024-05-22 09:33:50 +020075 url += "/{}".format(self.parent_id)
76 return self._handler("GET", url, params=params)
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040077
78 def find_all(self, **kwargs):
79 return self().find_all(**kwargs)
80
81 def find(self, **kwargs):
82 # if plan is searched perform an additional GET request to API
83 # in order to return full its data including 'entries' field
84 # see http://docs.gurock.com/testrail-api2/reference-plans#get_plans
85 if self._item_class is Plan:
86 return self.get(self().find(**kwargs).id)
87 return self().find(**kwargs)
88
89 def get(self, id):
90 return self._item_class.get(id)
91
92 def list(self):
93 name = self._item_class._api_name()
94 return ItemSet([self._item_class(**i) for i in self._list(name=name)])
95
96
97class Item(object):
stavrovska28772bc2024-05-22 09:33:50 +020098 _get_url = "get_{name}/{id}"
99 _update_url = "update_{name}/{id}"
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400100 _handler = None
stavrovska28772bc2024-05-22 09:33:50 +0200101 _repr_field = "name"
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400102
103 def __init__(self, id=None, **kwargs):
104 self.id = id
105 self._data = kwargs
106
107 @classmethod
108 def _api_name(cls):
109 return cls.__name__.lower()
110
111 def __getattr__(self, name):
112 if name in self._data:
113 return self._data[name]
114 else:
115 raise AttributeError
116
117 def __setattr__(self, name, value):
stavrovska28772bc2024-05-22 09:33:50 +0200118 if "_data" in self.__dict__ and name not in self.__dict__:
119 self.__dict__["_data"][name] = value
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400120 else:
121 self.__dict__[name] = value
122
123 def __repr__(self):
stavrovska28772bc2024-05-22 09:33:50 +0200124 name = getattr(self, self._repr_field, "")
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400125 name = repr(name)
stavrovska28772bc2024-05-22 09:33:50 +0200126 return "<{c.__name__}({s.id}) {name} at 0x{id:x}>".format(
127 s=self, c=self.__class__, id=id(self), name=name
128 )
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400129
130 @classmethod
131 def get(cls, id):
132 name = cls._api_name()
133 url = cls._get_url.format(name=name, id=id)
stavrovska28772bc2024-05-22 09:33:50 +0200134 result = cls._handler("GET", url)
135 if "error" in result:
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400136 raise Exception(result)
137 return cls(**result)
138
139 def update(self):
140 url = self._update_url.format(name=self._api_name(), id=self.id)
stavrovska28772bc2024-05-22 09:33:50 +0200141 self._handler("POST", url, json=self.data)
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400142
143 @property
144 def data(self):
145 return self._data
146
147
148class Project(Item):
149 @property
150 def suites(self):
151 return Collection(Suite, parent_id=self.id)
152
153
154class Suite(Item):
155 @property
156 def cases(self):
157 return CaseCollection(
158 Case,
stavrovska28772bc2024-05-22 09:33:50 +0200159 _list_url="get_cases/{}&suite_id={}".format(
160 self.project_id, self.id
161 ),
162 )
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400163
164
165class CaseCollection(Collection):
166 pass
167
168
169class Case(Item):
170 pass
171
172
173class Plan(Item):
stavrovska28772bc2024-05-22 09:33:50 +0200174 def __init__(
175 self,
176 name,
177 description=None,
178 milestone_id=None,
179 entries=None,
180 id=None,
181 **kwargs
182 ):
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400183 add_kwargs = {
stavrovska28772bc2024-05-22 09:33:50 +0200184 "name": name,
185 "description": description,
186 "milestone_id": milestone_id,
187 "entries": entries or [],
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400188 }
189 kwargs.update(add_kwargs)
190 return super(self.__class__, self).__init__(id, **kwargs)
191
192
193class Client(object):
194 def __init__(self, base_url, username, password):
195 self.username = username
196 self.password = password
stavrovska28772bc2024-05-22 09:33:50 +0200197 self.base_url = base_url.rstrip("/") + "/index.php?/api/v2/"
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400198
199 Item._handler = self._query
200
201 def _query(self, method, url, **kwargs):
202 url = self.base_url + url
stavrovska28772bc2024-05-22 09:33:50 +0200203 headers = {"Content-type": "application/json"}
204 logger.debug("Make {} request to {}".format(method, url))
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400205 for _ in range(5):
206 response = requests.request(
207 method,
208 url,
209 allow_redirects=False,
210 auth=(self.username, self.password),
211 headers=headers,
stavrovska28772bc2024-05-22 09:33:50 +0200212 **kwargs
213 )
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400214 # To many requests
215 if response.status_code == 429:
216 time.sleep(60)
217 continue
218 else:
219 break
220 # Redirect or error
221 if response.status_code >= 300:
stavrovska28772bc2024-05-22 09:33:50 +0200222 raise requests.HTTPError(
223 "Wrong response:\n"
224 "status_code: {0.status_code}\n"
225 "headers: {0.headers}\n"
226 "content: '{0.content}'".format(response),
227 response=response,
228 )
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400229 result = response.json()
stavrovska28772bc2024-05-22 09:33:50 +0200230 if "error" in result:
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +0400231 logger.warning(result)
232 return result
233
234 @property
235 def projects(self):
236 return Collection(Project)
237
238
239class NotFound(Exception):
240 def __init__(self, item_class, **conditions):
241 self.item_class = item_class
242 self.conditions = conditions
243
244 def __str__(self):
stavrovska28772bc2024-05-22 09:33:50 +0200245 conditions = ", ".join(
246 ['{}="{}"'.format(x, y) for (x, y) in self.conditions.items()]
247 )
248 return "{type} with {conditions}".format(
249 type=self.item_class._api_name().title(), conditions=conditions
250 )