blob: 94bc5f6abb0c7d53fee8a1510d0ce370cb71b2c3 [file] [log] [blame]
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +04001from __future__ import absolute_import
2import logging
3import time
4
5import requests
6
7logger = logging.getLogger(__name__)
8
9requests_logger = logging.getLogger('requests.packages.urllib3')
10requests_logger.setLevel(logging.WARNING)
11
12
13class ItemSet(list):
14 def __init__(self, *args, **kwargs):
15 self._item_class = None
16 return super(ItemSet, self).__init__(*args, **kwargs)
17
18 def find_all(self, **kwargs):
19 filtered = ItemSet(
20 x for x in self
21 if all(getattr(x, k) == v for k, v in kwargs.items()))
22 filtered._item_class = self._item_class
23 return filtered
24
25 def find(self, **kwargs):
26 items = self.find_all(**kwargs)
27 if items:
28 return items[0]
29 else:
30 raise NotFound(self._item_class, **kwargs)
31
32
33class Collection(object):
34
35 _list_url = 'get_{name}s'
36 _add_url = 'add_{name}'
37
38 def __init__(self, item_class=None, parent_id=None, **kwargs):
39 self._item_class = item_class
40 self._handler = self._item_class._handler
41 self.parent_id = parent_id
42 for k, v in kwargs.items():
43 setattr(self, k, v)
44
45 def __call__(self, id=None):
46 name = self._item_class._api_name()
47 if id is None:
48 items = self._list(name)
49 if 'error' in items:
50 raise Exception(items)
Ilya Menkov62bbd022021-09-21 19:09:55 +040051 if name == 'project':
52 items = items['projects']
53 if name == 'case':
54 items = items['cases']
Ilya Menkovbfc4c4e2019-10-17 17:08:17 +040055 items = ItemSet(self._to_object(x) for x in items)
56 items._item_class = self._item_class
57 return items
58
59 else:
60 return self._item_class.get(id)
61
62 def __repr__(self):
63 return '<Collection of {}>'.format(self._item_class.__name__)
64
65 def _to_object(self, data):
66 return self._item_class(**data)
67
68 def _list(self, name, params=None):
69 params = params or {}
70 url = self._list_url.format(name=name)
71 if self.parent_id is not None:
72 url += '/{}'.format(self.parent_id)
73 return self._handler('GET', url, params=params)
74
75 def find_all(self, **kwargs):
76 return self().find_all(**kwargs)
77
78 def find(self, **kwargs):
79 # if plan is searched perform an additional GET request to API
80 # in order to return full its data including 'entries' field
81 # see http://docs.gurock.com/testrail-api2/reference-plans#get_plans
82 if self._item_class is Plan:
83 return self.get(self().find(**kwargs).id)
84 return self().find(**kwargs)
85
86 def get(self, id):
87 return self._item_class.get(id)
88
89 def list(self):
90 name = self._item_class._api_name()
91 return ItemSet([self._item_class(**i) for i in self._list(name=name)])
92
93
94class Item(object):
95 _get_url = 'get_{name}/{id}'
96 _update_url = 'update_{name}/{id}'
97 _handler = None
98 _repr_field = 'name'
99
100 def __init__(self, id=None, **kwargs):
101 self.id = id
102 self._data = kwargs
103
104 @classmethod
105 def _api_name(cls):
106 return cls.__name__.lower()
107
108 def __getattr__(self, name):
109 if name in self._data:
110 return self._data[name]
111 else:
112 raise AttributeError
113
114 def __setattr__(self, name, value):
115 if '_data' in self.__dict__ and name not in self.__dict__:
116 self.__dict__['_data'][name] = value
117 else:
118 self.__dict__[name] = value
119
120 def __repr__(self):
121 name = getattr(self, self._repr_field, '')
122 name = repr(name)
123 return '<{c.__name__}({s.id}) {name} at 0x{id:x}>'.format(
124 s=self, c=self.__class__, id=id(self), name=name)
125
126 @classmethod
127 def get(cls, id):
128 name = cls._api_name()
129 url = cls._get_url.format(name=name, id=id)
130 result = cls._handler('GET', url)
131 if 'error' in result:
132 raise Exception(result)
133 return cls(**result)
134
135 def update(self):
136 url = self._update_url.format(name=self._api_name(), id=self.id)
137 self._handler('POST', url, json=self.data)
138
139 @property
140 def data(self):
141 return self._data
142
143
144class Project(Item):
145 @property
146 def suites(self):
147 return Collection(Suite, parent_id=self.id)
148
149
150class Suite(Item):
151 @property
152 def cases(self):
153 return CaseCollection(
154 Case,
155 _list_url='get_cases/{}&suite_id={}'.format(self.project_id,
156 self.id))
157
158
159class CaseCollection(Collection):
160 pass
161
162
163class Case(Item):
164 pass
165
166
167class Plan(Item):
168 def __init__(self,
169 name,
170 description=None,
171 milestone_id=None,
172 entries=None,
173 id=None,
174 **kwargs):
175 add_kwargs = {
176 'name': name,
177 'description': description,
178 'milestone_id': milestone_id,
179 'entries': entries or [],
180 }
181 kwargs.update(add_kwargs)
182 return super(self.__class__, self).__init__(id, **kwargs)
183
184
185class Client(object):
186 def __init__(self, base_url, username, password):
187 self.username = username
188 self.password = password
189 self.base_url = base_url.rstrip('/') + '/index.php?/api/v2/'
190
191 Item._handler = self._query
192
193 def _query(self, method, url, **kwargs):
194 url = self.base_url + url
195 headers = {'Content-type': 'application/json'}
196 logger.debug('Make {} request to {}'.format(method, url))
197 for _ in range(5):
198 response = requests.request(
199 method,
200 url,
201 allow_redirects=False,
202 auth=(self.username, self.password),
203 headers=headers,
204 **kwargs)
205 # To many requests
206 if response.status_code == 429:
207 time.sleep(60)
208 continue
209 else:
210 break
211 # Redirect or error
212 if response.status_code >= 300:
213 raise requests.HTTPError("Wrong response:\n"
214 "status_code: {0.status_code}\n"
215 "headers: {0.headers}\n"
216 "content: '{0.content}'".format(response),
217 response=response)
218 result = response.json()
219 if 'error' in result:
220 logger.warning(result)
221 return result
222
223 @property
224 def projects(self):
225 return Collection(Project)
226
227
228class NotFound(Exception):
229 def __init__(self, item_class, **conditions):
230 self.item_class = item_class
231 self.conditions = conditions
232
233 def __str__(self):
234 conditions = ', '.join(['{}="{}"'.format(x, y)
235 for (x, y) in self.conditions.items()])
236 return u'{type} with {conditions}'.format(
237 type=self.item_class._api_name().title(),
238 conditions=conditions)