client.py 13.8 KB
Newer Older
1
import logging
Jo Sama's avatar
Jo Sama committed
2
import requests
3
import uuid
4
import json
Jo Sama's avatar
Jo Sama committed
5

6
from typing import Optional, List, Iterator
Jo Sama's avatar
Jo Sama committed
7
from urllib.parse import urljoin, urlparse
Jo Sama's avatar
Jo Sama committed
8

9
10
from .models import (Branch, BranchReference, Operator, OperatorGroup, PermissionGroup,
                     Person, Department, DepartmentRef, BudgetHolder, BudgetHolderRef)
Jo Sama's avatar
Jo Sama committed
11

12
13
logger = logging.getLogger(__name__)

Jo Sama's avatar
Jo Sama committed
14
15
16
17
18
19
20
21

class Endpoints:
    def __init__(self, base_url):
        self.base_url = base_url

    def _prepend_base_url(self, path):
        return urljoin(self.base_url, path)

22
23
24
    def list_branches(self):
        return self._prepend_base_url('branches/')

25
26
27
    def list_operatorgroups(self):
        return self._prepend_base_url('operatorgroups')

28
29
30
    def list_permissiongroups(self):
        return self._prepend_base_url('permissiongroups')

31
32
33
    def get_branch(self, branch_id):
        return urljoin(self._prepend_base_url('branches/id/'), branch_id)

34
35
36
37
38
39
    def get_departments(self):
        return self._prepend_base_url('departments')

    def get_budget_holder(self):
        return self._prepend_base_url('budgetholders')

Jo Sama's avatar
Jo Sama committed
40
    def get_operator(self, identity):
Jo Sama's avatar
Jo Sama committed
41
        return urljoin(self._prepend_base_url('operators/id/'), identity)
Jo Sama's avatar
Jo Sama committed
42

Jo Sama's avatar
Jo Sama committed
43
    def get_operators(self):
Jo Sama's avatar
Jo Sama committed
44
        return self._prepend_base_url('operators/')
Jo Sama's avatar
Jo Sama committed
45

Jo Sama's avatar
Jo Sama committed
46
47
48
    def get_operator_filters(self):
        return self._prepend_base_url('operators/filters/operator')

49
50
51
    def get_operator_filter(self, identity):
        return urljoin(self.get_operator(identity) + '/', 'filters/operator')

Jo Sama's avatar
Jo Sama committed
52
    def get_person(self, identity):
Jo Sama's avatar
Jo Sama committed
53
        return urljoin(self._prepend_base_url('persons/id/'), identity)
Jo Sama's avatar
Jo Sama committed
54
55

    def get_persons(self):
Jo Sama's avatar
Jo Sama committed
56
        return self._prepend_base_url('persons/')
Jo Sama's avatar
Jo Sama committed
57

58
59
60
61
62
63
64
65
66
67
68
69
    def get_operators_operatorgroup(self, operator_id):
        return urljoin(
            urljoin(self._prepend_base_url('operators/id/'),
                    operator_id + '/'),
            'operatorgroups')

    def get_operators_permissiongroup(self, operator_id):
        return urljoin(
            urljoin(self._prepend_base_url('operators/id/'),
                    operator_id + '/'),
            'permissiongroups')

Jo Sama's avatar
Jo Sama committed
70

Jo Sama's avatar
Jo Sama committed
71
class TopDeskClient:
Jo Sama's avatar
Jo Sama committed
72
73
74
75
    def __init__(self,
                 url,
                 username,
                 password,
76
                 headers={},
Jo Sama's avatar
Jo Sama committed
77
                 rewrite_url=None):
Jo Sama's avatar
Jo Sama committed
78
        self.urls = Endpoints(url)
79
80
        self.rewrite_url = rewrite_url
        self.headers = headers
Jo Sama's avatar
Jo Sama committed
81
        self.auth = requests.auth.HTTPBasicAuth(username, password)
82
83
84
85
86
87
88
89

    def _build_headers(self, headers):
        request_headers = {}
        for h in (self.headers):
            request_headers[h] = self.headers[h]
        for h in (headers or ()):
            request_headers[h] = headers[h]
        return request_headers
Jo Sama's avatar
Jo Sama committed
90

91
92
93
94
95
    def call(self,
             method_name,
             url,
             headers=None,
             params=None,
Jo Sama's avatar
Jo Sama committed
96
             return_response=False,
97
98
99
100
101
             **kwargs):
        logger.debug('Calling %s %s with params=%r',
                     method_name,
                     urlparse(url).path,
                     params)
Jo Sama's avatar
Jo Sama committed
102
        r = requests.request(method_name,
Jo Sama's avatar
Jo Sama committed
103
                             (url if self.rewrite_url is None
104
                              else url.replace(*self.rewrite_url)),
Jo Sama's avatar
Jo Sama committed
105
                             auth=self.auth,
Jo Sama's avatar
Jo Sama committed
106
107
                             headers=(None if headers is None else
                                      self._build_headers(headers)),
108
                             params=params if params is not None else {},
Jo Sama's avatar
Jo Sama committed
109
                             **kwargs)
110
        logger.debug('GOT HTTP %d: %r', r.status_code, r.content)
Jo Sama's avatar
Jo Sama committed
111
112
113
114
115
116

        if return_response:
            return r
        else:
            r.raise_for_status()
            return r.json()
Jo Sama's avatar
Jo Sama committed
117
118
119
120
121
122
123
124
125
126

    def post(self, url, **kwargs):
        return self.call('POST', url, **kwargs)

    def get(self, url, **kwargs):
        return self.call('GET', url, **kwargs)

    def put(self, url, **kwargs):
        return self.call('PUT', url, **kwargs)

Jo Sama's avatar
Jo Sama committed
127
128
129
    def patch(self, url, **kwargs):
        return self.call('PATCH', url, **kwargs)

130
131
132
    def delete(self, url, **kwargs):
        return self.call('DELETE', url, **kwargs)

133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
    def depaginate(self, url, page_size=100):
        offset = 0
        while True:
            params = {
                'page_size': page_size,
                'start': offset,
            }
            page = self.get(url, params=params, return_response=True)
            if page.status_code == 204:
                break
            data = page.json()
            for item in data:
                yield item
            num = len(data)
            if num < page_size:
                break
            if num == page_size:
                offset += num

152
153
154
155
156
    def include_fields(self, params: dict, fields: Optional[List[str]] = None) -> None:
        if not fields:
            return
        params['$fields'] = ','.join(fields)

157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
    def create_department(self, department):
        url = self.urls.get_departments()
        r = self.post(url,
                      return_response=True,
                      headers={'Content-Type': 'application/json'},
                      data=department.json())
        if r.status_code == 201:
            return DepartmentRef.from_json(r.content)
        else:
            r.raise_for_status()
            return None

    def get_department(self, department):
        url = self.urls.get_departments()
        r = self.get(url, return_response=True)
        if r.status_code == 200:
            for x in r.json():
                if x.get('name') == department:
                    return DepartmentRef.from_dict(x)
            return None
        elif r.status_code in (404, 500):
            return None
        else:
            r.raise_for_status()
            return None

    def create_budget_holder(self, budget_holder):
        url = self.urls.get_budget_holder()
        r = self.post(url,
                      return_response=True,
                      headers={'Content-Type': 'application/json'},
                      data=budget_holder.json())
        if r.status_code == 201:
            return BudgetHolderRef.from_json(r.content)
        else:
            r.raise_for_status()
            return None

    def get_budget_holder(self, budget_holder):
        url = self.urls.get_budget_holder()
        r = self.get(url, return_response=True)
        if r.status_code == 200:
            for x in r.json():
                if x.get('name') == budget_holder:
                    return BudgetHolderRef.from_dict(x)
            return None
        elif r.status_code == 404:
            return None
        else:
            r.raise_for_status()
            return None

209
    def get_operator(self, identity):
210
211
        def _get_operator_by_id(ident):
            url = self.urls.get_operator(ident)
212
213
214
215
            r = self.get(url, return_response=True)
            if r.status_code == 200:
                return Operator.from_dict(r.json())
            else:
Jo Sama's avatar
Jo Sama committed
216
                r.raise_for_status()
217
                return None
218
219

        def _get_operator_by_username(name):
220
            url = self.urls.get_operators()
Jo Sama's avatar
Jo Sama committed
221
222
223
224
225
226
227
228
            r = self.get(url,
                         params={'topdesk_login_name': name},
                         return_response=True)
            if r.status_code == 204:
                return None
            else:
                r.raise_for_status()

229
            f = filter(lambda x: x.user_name.lower() == name,
Jo Sama's avatar
Jo Sama committed
230
                       [Operator.from_dict(x) for x in r.json()])
231
232
233
234
            try:
                return next(f)
            except StopIteration:
                return None
Jo Sama's avatar
Jo Sama committed
235

236
237
238
239
240
241
242
        if isinstance(identity, Operator) and identity.id:
            return _get_operator_by_id(identity.id)
        elif isinstance(identity, Operator):
            return _get_operator_by_username(identity.user_name)
        else:
            return _get_operator_by_username(identity)

Jo Sama's avatar
Jo Sama committed
243
244
    def create_operator(self, operator):
        url = self.urls.get_operators()
245
246
247
        # TODO: Remove password setting when proper auth arrives
        op = operator.copy()
        op.password = uuid.uuid4()
248
249
250
251
252
253
254
        r = self.post(url,
                      return_response=True,
                      data=op.json())
        if r.status_code == 201:
            return Operator.from_dict(r.json())
        else:
            return None
Jo Sama's avatar
Jo Sama committed
255

Jo Sama's avatar
Jo Sama committed
256
257
    def update_operator(self, operator):
        url = self.urls.get_operator(operator.id)
258
259
260
        # TODO: Remove password setting when proper auth arrives
        op = operator.copy()
        op.password = uuid.uuid4()
261
262
263
        r = self.put(url,
                     return_response=True,
                     data=op.json())
264
265
266
267
        if r.status_code == 200:
            return Operator.from_dict(r.json())
        else:
            return None
Jo Sama's avatar
Jo Sama committed
268

269
270
271
272
273
274
275
276
277
278
279
    def link_operator_to_operator_filter(self, operator, filter_id):
        url = self.urls.get_operator_filter(operator.id)
        r = self.post(url,
                      headers={'Content-Type': 'application/json'},
                      return_response=True,
                      data=json.dumps([{'id': filter_id}]))
        if r.status_code == 204:
            return True
        else:
            return [x.get('message') for x in r.json()]

Jo Sama's avatar
Jo Sama committed
280
281
282
283
    def list_operator_filters(self):
        return self.get(self.urls.get_operator_filters(),
                        return_response=True).json()

Jo Sama's avatar
Jo Sama committed
284
    def list_operators(self):
Jo Sama's avatar
Jo Sama committed
285
286
        for x in self.get(self.urls.get_operators()):
            yield Operator.from_dict(x)
Jo Sama's avatar
Jo Sama committed
287

288
289
290
291
292
    def list_operatorgroups(self):
        url = self.urls.list_operatorgroups()
        for x in self.depaginate(url):
            yield OperatorGroup.from_dict(x)

293
294
295
296
297
    def list_permissiongroups(self):
        url = self.urls.list_permissiongroups()
        for x in self.depaginate(url):
            yield PermissionGroup.from_dict(x)

Jo Sama's avatar
Jo Sama committed
298
    def get_person(self, identity):
299
300
301
302
303
304
305
306
307
        if isinstance(identity, Person):
            url = self.urls.get_person(identity.id)
            r = self.get(url, return_response=True)
            if r.status_code == 200:
                return Person.from_dict(r.json())
            else:
                return None
        else:
            url = self.urls.get_persons()
Jo Sama's avatar
Jo Sama committed
308
309
310
311
312
313
314
315
316
317
318
319
320
321
            r = self.get(url,
                         params={'ssp_login_name': identity},
                         return_response=True)

            if r.status_code == 200:
                persons = [Person.from_dict(x) for x in r.json()]
                f = filter(lambda x: x.user_name.lower() == identity,
                           persons)
                try:
                    return next(f)
                except StopIteration:
                    return None
            else:
                r.raise_for_status()
322
                return None
Jo Sama's avatar
Jo Sama committed
323
324
325

    def create_person(self, person):
        url = self.urls.get_persons()
326
327
328
329
330
331
        # TODO: Remove password setting when proper auth arrives
        pe = person.copy()
        pe.password = uuid.uuid4()
        return self.post(url,
                         return_response=True,
                         data=pe.json())
Jo Sama's avatar
Jo Sama committed
332
333
334

    def update_person(self, person):
        url = self.urls.get_person(person.id)
335
336
337
338
339
340
        # TODO: Remove password setting when proper auth arrives
        pe = person.copy()
        pe.password = uuid.uuid4()
        return self.patch(url,
                          return_response=True,
                          data=pe.json())
Jo Sama's avatar
Jo Sama committed
341
342
343
344
345

    def list_persons(self):
        for x in self.get(self.urls.get_persons()):
            yield Person.from_dict(x)

346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
    def list_branches(self, fields: Optional[list] = None, name_filter: Optional[str] = None) -> Iterator[BranchReference]:
        params = {}
        self.include_fields(params,
                            fields or ['id', 'name', 'clientReferenceNumber'])
        if name_filter:
            params['nameFragment'] = name_filter
        for x in self.get(self.urls.list_branches(), params=params):
            yield BranchReference.from_dict(x)

    def get_branch(self, branch_id: str) -> Optional[Branch]:
        url = self.urls.get_branch(branch_id)
        r = self.get(url, return_response=True)
        if r.status_code == 200:
            return Branch.from_dict(r.json())
        return None

    def create_branch(self, branch: Branch):
        url = self.urls.list_branches()
        return self.post(url,
                         return_response=True,
                         data=branch.json())

368
369
370
371
372
373
374
375
376
377
    def patch_branch(self, branch_id: str, data: dict) -> Optional[Branch]:
        url = self.urls.get_branch(branch_id)
        r = self.patch(url,
                       return_response=True,
                       json=data)
        r.raise_for_status()
        if r.status_code == 200:
            return Branch.from_dict(r.json())
        return None

378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
    def _do_group_op(self, url_generator, operator_id, group_id, operation):
        url = url_generator(operator_id)
        r = operation(url,
                      return_response=True,
                      json=[{'id': group_id}])

        if r.status_code == 204:
            return None
        else:
            r.raise_for_status()
            return r


    def add_operator_to_operator_group(self, operator: Operator, operator_group: OperatorGroup):
        return self._do_group_op(self.urls.get_operators_operatorgroup, operator.id, operator_group.id, self.post)

    def add_operator_to_permission_group(self, operator: Operator, permission_group: PermissionGroup):
        return self._do_group_op(self.urls.get_operators_permissiongroup, operator.id, permission_group.id, self.post)

    def remove_operator_from_operator_group(self, operator: Operator, operator_group: OperatorGroup):
        return self._do_group_op(self.urls.get_operators_operatorgroup, operator.id, operator_group.id, self.delete)

    def remove_operator_from_permission_group(self, operator: Operator, permission_group: PermissionGroup):
        return self._do_group_op(self.urls.get_operators_permissiongroup, operator.id, permission_group.id, self.delete)
Jo Sama's avatar
Jo Sama committed
402
403
404

def get_client(config):
    return TopDeskClient(**config)