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