gcis_client.py 19.1 KB
Newer Older
1
import json
2
3
import os
from os.path import basename, expanduser
abuddenberg's avatar
abuddenberg committed
4
import re
5
import requests
6
7
import yaml
import getpass
8

abuddenberg's avatar
abuddenberg committed
9
from domain import Figure, Image, Dataset, Activity, Person, Organization
10
11
12
13
14
15


def check_image(fn):
    def wrapped(*args, **kwargs):
        # if len(args) < 1 or not isinstance(args[0], Image):
        #     raise Exception('Invalid Image')
16
        if args[1].identifier in (None, ''):
17
            raise Exception('Invalid identifier', args[0].identifier)
18
        return fn(*args, **kwargs)
19
20
21
22

    return wrapped


23
24
25
26
27
28
29
30
31
32
33
34
def exists(fn):
    def wrapped(*args, **kwargs):
        resp = fn(*args, **kwargs)
        if resp.status_code == 200:
            return True
        elif resp.status_code == 404:
            return False
        else:
            raise Exception(resp.text)
    return wrapped


abuddenberg's avatar
abuddenberg committed
35
36
37
38
39
40
41
42
43
44
def http_resp(fn):
    def wrapped(*args, **kwargs):
        resp = fn(*args, **kwargs)
        if resp.status_code == 200:
            return resp
        else:
            raise Exception(resp.text)
    return wrapped


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def get_credentials(url):
    #First check our magic enviroment variables (GCIS_USER and GCIS_KEY)
    from gcis_clients import gcis_auth
    env_user, env_key = gcis_auth

    if env_user is not None and env_key is not None:
        return env_user, env_key

    #Next, see if we can find Gcis.conf somewhere
    conf_possibilities = [expanduser(f) for f in ['~/etc/Gcis.conf', '~/.gcis-py-client/Gcis.conf'] if
                          os.path.exists(expanduser(f))]

    for gcis_config in conf_possibilities:
        print 'Using {gc} for credentials...'.format(gc=gcis_config)
        all_creds = yaml.load(open(gcis_config, 'r'))
        instance_creds = [c for c in all_creds if c['url'] == url][0]

        return instance_creds['userinfo'].split(':')[0], instance_creds['key']

    #Else prompt for credentials
    #Wow, I managed to use it
    else:
        username = raw_input('Username: ')
        api_key = getpass.getpass('API key: ')

        return username, api_key


73
74
75
76
77
78
79
80
class AssociationException(Exception):
    def __init__(self, value):
        self.value = value

    def __str__(self):
        return repr(self.value)


81
class GcisClient(object):
82
    def __init__(self, url, username, api_key):
83
        self.base_url = url
84
85
86
87
88

        #If credentials were not provided, obtain them
        if username is None or api_key is None:
            username, api_key = get_credentials(url)

89
        self.s = requests.Session()
90
        self.s.auth = (username, api_key)
91
        self.s.headers.update({'Accept': 'application/json'})
92

93
    @http_resp
94
95
    def create_figure(self, report_id, chapter_id, figure, skip_images=False):
        if figure.identifier in (None, ''):
96
97
98
99
100
            raise Exception('Invalid figure identifier', figure.identifier)

        #Is GCIS not inferring this from the url parameter?
        if figure.chapter_identifier in (None, ''):
            figure.chapter_identifier = chapter_id
101

102
103
        url = '{b}/report/{rpt}/chapter/{chp}/figure/'.format(
            b=self.base_url, rpt=report_id, chp=chapter_id
104
105
        )

106
        resp = self.s.post(url, data=figure.as_json(), verify=False)
107
108

        if skip_images is False:
109
            for image in figure.images:
110
111
                self.create_image(image),
                self.associate_image_with_figure(image.identifier, report_id, figure.identifier)
112

113
        return resp
114

115
    @http_resp
116
    def update_figure(self, report_id, chapter_id, figure, skip_images=False, old_id=None):
117
118
        if figure.identifier in (None, ''):
            raise Exception('Invalid identifier', figure.identifier)
119
120
121
122
123

        #Is GCIS not inferring this from the url parameter?
        if figure.chapter_identifier in (None, ''):
            figure.chapter_identifier = chapter_id

124
125
        url = '{b}/report/{rpt}/chapter/{chp}/figure/{fig}'.format(
            b=self.base_url, rpt=report_id, chp=chapter_id, fig=old_id or figure.identifier
126
127
        )

128
        resp = self.s.post(url, data=figure.as_json(), verify=False)
129

130
        if skip_images is False:
131
            for image in figure.images:
132
133
                self.update_image(image)

134
135
136
        for c in figure.contributors:
            self.associate_contributor_with_figure(c, report_id, chapter_id, figure.identifier)

137
        return resp
138

139
    @http_resp
140
141
    def delete_figure(self, report_id, figure_id):
        url = '{b}/report/{rpt}/figure/{fig}'.format(b=self.base_url, rpt=report_id, fig=figure_id)
142
        return self.s.delete(url, verify=False)
143

144
    @check_image
145
    def create_image(self, image, report_id=None, figure_id=None):
abuddenberg's avatar
abuddenberg committed
146
        url = '{b}/image/'.format(b=self.base_url)
147
        resp = self.s.post(url, data=image.as_json(), verify=False)
148
149
150
151
152
153
        
        if image.local_path is not None:
            self.upload_image_file(image.identifier, image.local_path)
        if figure_id and report_id:
            self.associate_image_with_figure(image.identifier, report_id, figure_id)
        for dataset in image.datasets:
abuddenberg's avatar
abuddenberg committed
154
155
            if not self.dataset_exists(dataset.identifier):
                self.create_dataset(dataset)
156
157
158
            # if not self.activity_exists(dataset.activity.identifier):
            #     self.create_activity(dataset.activity))
            self.create_or_update_activity(dataset.activity)
159
160
            self.associate_dataset_with_image(dataset.identifier, image.identifier,
                                              activity_id=dataset.activity.identifier)
abuddenberg's avatar
abuddenberg committed
161
        return resp
162

163
    @check_image
164
165
    def update_image(self, image, old_id=None):
        url = '{b}/image/{img}'.format(b=self.base_url, img=old_id or image.identifier)
166
        for dataset in image.datasets:
167
168
            # self.update_activity(dataset.activity)
            self.create_or_update_activity(dataset.activity)
abuddenberg's avatar
abuddenberg committed
169
170
            self.associate_dataset_with_image(dataset.identifier, image.identifier,
                                              activity_id=dataset.activity.identifier)
171
172
        for c in image.contributors:
            self.associate_contributor_with_image(c, image.identifier)
173

174
        return self.s.post(url, data=image.as_json(), verify=False)
175

176
    @check_image
177
    @http_resp
178
179
    def delete_image(self, image):
        delete_url = '{b}/image/{img}'.format(b=self.base_url, img=image.identifier)
180
        return self.s.delete(delete_url, verify=False)
181

182
    @http_resp
183
184
    def associate_image_with_figure(self, image_id, report_id, figure_id):
        url = '{b}/report/{rpt}/figure/rel/{fig}'.format(b=self.base_url, rpt=report_id, fig=figure_id)
185
        return self.s.post(url, data=json.dumps({'add_image_identifier': image_id}), verify=False)
186

187
    @http_resp
188
    def upload_image_file(self, image_id, local_path):
189
        url = '{b}/image/files/{id}/{fn}'.format(b=self.base_url, id=image_id, fn=basename(local_path))
190
        # For future multi-part encoding support
191
        # return self.s.put(url, headers=headers, files={'file': (filename, open(filepath, 'rb'))})
192
        if not os.path.exists(local_path):
193
194
            raise Exception('File not found: ' + local_path)

195
        return self.s.put(url, data=open(local_path, 'rb'), verify=False)
196

197
198
199
    #Full listing
    def get_figure_listing(self, report_id, chapter_id=None):
        chapter_filter = '/chapter/' + chapter_id if chapter_id else ''
200

201
202
        url = '{b}/report/{rpt}{chap}/figure'.format(b=self.base_url, rpt=report_id, chap=chapter_filter)
        resp = self.s.get(url, params={'all': '1'}, verify=False)
203

204
205
206
        try:
            return [Figure(figure) for figure in resp.json()]
        except ValueError:
207
            raise Exception(resp.text)
208

209
210
    def get_figure(self, report_id, figure_id, chapter_id=None):
        chapter_filter = '/chapter/' + chapter_id if chapter_id else ''
211

212
213
        url = '{b}/report/{rpt}{chap}/figure/{fig}'.format(
            b=self.base_url, rpt=report_id, chap=chapter_filter, fig=figure_id
214
        )
215
        resp = self.s.get(url, params={'all': '1'}, verify=False)
216

217
218
219
220
        try:
            return Figure(resp.json())
        except ValueError:
            raise Exception(resp.text)
221

222
223
224
225
    @exists
    def figure_exists(self, report_id, figure_id, chapter_id=None):
        chapter_filter = '/chapter/' + chapter_id if chapter_id else ''

226
227
        url = '{b}/report/{rpt}{chap}/figure/{fig}'.format(
            b=self.base_url, rpt=report_id, chap=chapter_filter, fig=figure_id
228
        )
229
        return self.s.head(url, verify=False)
230

231
232
    def get_image(self, image_id):
        url = '{b}/image/{img}'.format(b=self.base_url, img=image_id)
233
        resp = self.s.get(url, verify=False)
234

235
236
237
238
239
240
241
242
        try:
            return Image(resp.json())
        except ValueError:
            raise Exception(resp.text)

    @exists
    def image_exists(self, image_id):
        url = '{b}/image/{img}'.format(b=self.base_url, img=image_id)
243
        return self.s.head(url, verify=False)
244

245
246
    def has_all_associated_images(self, report_id, figure_id, target_image_ids):
        try:
247
248
249
250
            figure_image_ids = [i.identifier for i in self.get_figure(report_id, figure_id).images]
        except Exception, e:
            print e.message
            return False, set()
251

252
253
254
255
256
257
        target_set = set(target_image_ids)
        gcis_set = set(figure_image_ids)
        deltas = target_set - gcis_set

        if target_set.issubset(gcis_set):
            return True, deltas
258
        else:
259
            return False, deltas
260

261
262
    def test_login(self):
        url = '{b}/login.json'.format(b=self.base_url)
263
        resp = self.s.get(url, verify=False)
264
265
266
        return resp.status_code, resp.text

    def get_keyword_listing(self):
267
268
        url = '{b}/gcmd_keyword'.format(b=self.base_url)
        resp = self.s.get(url, params={'all': '1'}, verify=False)
269

270
271
272
273
        return resp.json()

    def get_keyword(self, key_id):
        url = '{b}/gcmd_keyword/{k}'.format(b=self.base_url, k=key_id)
274
        return self.s.get(url, verify=False).json()
275
276
277

    def associate_keyword_with_figure(self, keyword_id, report_id, figure_id):
        url = '{b}/report/{rpt}/figure/keywords/{fig}'.format(b=self.base_url, rpt=report_id, fig=figure_id)
278
        return self.s.post(url, data=json.dumps({'identifier': keyword_id}), verify=False)
279
280
281

    def get_dataset(self, dataset_id):
        url = '{b}/dataset/{ds}'.format(b=self.base_url, ds=dataset_id)
282
        resp = self.s.get(url, verify=False)
283
284
285
        try:
            return Dataset(resp.json())
        except ValueError:
abuddenberg's avatar
abuddenberg committed
286
            raise Exception(resp.text)
287

288
289
290
    @exists
    def dataset_exists(self, dataset_id):
        url = '{b}/dataset/{ds}'.format(b=self.base_url, ds=dataset_id)
291
        return self.s.head(url, verify=False)
292

abuddenberg's avatar
abuddenberg committed
293
    def create_dataset(self, dataset):
294
        url = '{b}/dataset/'.format(b=self.base_url)
295
        return self.s.post(url, data=dataset.as_json(), verify=False)
296

297
298
    def update_dataset(self, dataset, old_id=None):
        url = '{b}/dataset/{ds}'.format(b=self.base_url, ds=old_id or dataset.identifier)
299
        return self.s.post(url, data=dataset.as_json(), verify=False)
300
301
302

    def delete_dataset(self, dataset):
        url = '{b}/dataset/{ds}'.format(b=self.base_url, ds=dataset.identifier)
303
        return self.s.delete(url, verify=False)
304

abuddenberg's avatar
abuddenberg committed
305
    def associate_dataset_with_image(self, dataset_id, image_id, activity_id=None):
306
        url = '{b}/image/prov/{img}'.format(b=self.base_url, img=image_id)
abuddenberg's avatar
abuddenberg committed
307

308
309
310
311
        data = {
            'parent_uri': '/dataset/' + dataset_id,
            'parent_rel': 'prov:wasDerivedFrom'
        }
abuddenberg's avatar
abuddenberg committed
312
313
314
        if activity_id:
            data['activity'] = activity_id

315
316
317
318
319
        try:
            self.delete_dataset_image_assoc(dataset_id, image_id)
        except AssociationException as e:
            print e.value

320
        resp = self.s.post(url, data=json.dumps(data), verify=False)
321
322
323
324

        if resp.status_code == 200:
            return resp
        else:
325
            raise Exception('Dataset association failed:\n{url}\n{resp}'.format(url=url, resp=resp.text))
326

abuddenberg's avatar
abuddenberg committed
327
328
329
330
331
332
333
334
335
    def delete_dataset_image_assoc(self, dataset_id, image_id):
        url = '{b}/image/prov/{img}'.format(b=self.base_url, img=image_id)

        data = {
            'delete': {
                'parent_uri': '/dataset/' + dataset_id,
                'parent_rel': 'prov:wasDerivedFrom'
            }
        }
336
        resp = self.s.post(url, data=json.dumps(data), verify=False)
abuddenberg's avatar
abuddenberg committed
337
338
339
340

        if resp.status_code == 200:
            return resp
        else:
341
342
343
344
345
346
            raise AssociationException(
                'Dataset dissociation failed:\n{url}\n{resp}\n{d}'.format(url=url, resp=resp.text, d=data))

    def create_or_update_dataset(self, dataset):
        if self.dataset_exists(dataset.identifier):
            print 'Updating dataset: ' + dataset.identifier
347
            self.update_dataset(dataset)
348
349
        else:
            print 'Creating dataset: ' + dataset.identifier
350
            self.create_dataset(dataset)
351

abuddenberg's avatar
abuddenberg committed
352
353
354
    # @exists
    def activity_exists(self, activity_id):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=activity_id)
355
        resp = self.s.head(url, verify=False)
abuddenberg's avatar
abuddenberg committed
356
357
358
359
360
361
362
        if resp.status_code == 200:
            return True
        else:
            return False

    def get_activity(self, activity_id):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=activity_id)
363
        resp = self.s.get(url, verify=False)
abuddenberg's avatar
abuddenberg committed
364
365
366
        try:
            return Activity(resp.json())
        except ValueError:
abuddenberg's avatar
abuddenberg committed
367
            raise Exception(resp.text)
abuddenberg's avatar
abuddenberg committed
368
369
370
371

    @http_resp
    def create_activity(self, activity):
        url = '{b}/activity/'.format(b=self.base_url)
372
        return self.s.post(url, data=activity.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
373
374

    @http_resp
375
376
    def update_activity(self, activity, old_id=None):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=old_id or activity.identifier)
377
        return self.s.post(url, data=activity.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
378
379
380
381

    @http_resp
    def delete_activity(self, activity):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=activity.identifier)
382
        return self.s.delete(url, verify=False)
abuddenberg's avatar
abuddenberg committed
383

384
385
386
387
388
    def create_or_update_activity(self, activity):
        if self.activity_exists(activity.identifier):
            self.update_activity(activity)
        else:
            self.create_activity(activity)
abuddenberg's avatar
abuddenberg committed
389

390
391
392
393
394
    def get_activity_list(self):
        url = '{b}/activity'.format(b=self.base_url)

        return self.s.get(url, params={'all': 1}, verify=False).json()

abuddenberg's avatar
abuddenberg committed
395
396
397
    @exists
    def person_exists(self, person_id):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person_id)
398
        return self.s.head(url, verfiy=False)
abuddenberg's avatar
abuddenberg committed
399
400
401

    def get_person(self, person_id):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person_id)
402
        resp = self.s.get(url, verify=False)
abuddenberg's avatar
abuddenberg committed
403
404
405
406
407
408
409
        try:
            return Person(resp.json())
        except ValueError:
            raise Exception(resp.text)

    def lookup_person(self, name):
        url = '{b}/autocomplete'.format(b=self.base_url)
410
        resp = self.s.get(url, params={'q': name, 'items': 15, 'type': 'person'}, verify=False)
abuddenberg's avatar
abuddenberg committed
411
412
413
414
415
416
417
418
419

        if resp.status_code == 200:
            return [re.match(r'\[person\] \{(\d+)\} (.*)', r).groups() for r in resp.json()]
        else:
            raise Exception(resp.text)

    @http_resp
    def create_person(self, person):
        url = '{b}/person/'.format(b=self.base_url)
420
        return self.s.post(url, data=person.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
421
422

    @http_resp
423
424
    def update_person(self, person, old_id=None):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=old_id or person.identifier)
425
        return self.s.post(url, data=person.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
426
427
428
429

    @http_resp
    def delete_person(self, person):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person.identifier)
430
        return self.s.delete(url, verify=False)
abuddenberg's avatar
abuddenberg committed
431
432
433
434

    @exists
    def organization_exists(self, org_id):
        url = '{b}/organization/{org_id)'.format(b=self.base_url, org_id=org_id)
435
        return self.s.head(url, verify=False)
abuddenberg's avatar
abuddenberg committed
436
437

    def get_organization(self, org_id):
438
439
        url = '{b}/organization/{org_id}'.format(b=self.base_url, org_id=org_id)
        resp = self.s.get(url, verify=False)
abuddenberg's avatar
abuddenberg committed
440
441
442
443
444
445
446

        try:
            return Organization(resp.json())
        except ValueError:
            raise Exception(resp.text)

    def lookup_organization(self, name):
447
        url = '{b}/autocomplete'.format(b=self.base_url)
448
        resp = self.s.get(url, params={'q': name, 'items': 15, 'type': 'organization'}, verify=False)
449
450
451
452
453
        
        if resp.status_code == 200:
            return [re.match(r'\[organization\] \{(.*)\} (.*)', r).groups() for r in resp.json()]
        else:
            raise Exception(resp.text)
abuddenberg's avatar
abuddenberg committed
454
455
456
457

    @http_resp
    def create_organization(self, org):
        url = '{b}/organization/'.format(b=self.base_url)
458
        return self.s.post(url, data=org.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
459
460

    @http_resp
461
462
    def update_organization(self, org, old_id=None):
        url = '{b}/organization/{org_id}'.format(b=self.base_url, org_id=old_id or org.identifier)
463
        return self.s.post(url, data=org.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
464
465
466
467

    @http_resp
    def delete_organization(self, org):
        url = '{b}/organization/{org_id}'.format(b=self.base_url, org_id=org.identifier)
468
        return self.s.delete(url, verify=False)
469
470
471
472
473

    @http_resp
    def associate_contributor_with_figure(self, contrib, report_id, chapter_id, figure_id):
        url = '{b}/report/{rpt}/chapter/{chp}/figure/contributors/{fig}'.format(b=self.base_url, rpt=report_id, chp=chapter_id, fig=figure_id)

474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
        data = {
            'role': contrib.role.type_id,
        }

        if contrib.person is not None and contrib.person.id is not None:
            data['person_id'] = contrib.person.id
        if contrib.organization is not None and contrib.organization.identifier:
            data['organization_identifier'] = contrib.organization.identifier

        resp = self.s.post(url, data=json.dumps(data), verify=False)
        return resp

    @http_resp
    def delete_contributor_figure_assoc(self, contrib, report_id, chapter_id, figure_id):
        url = '{b}/report/{rpt}/chapter/{chp}/figure/contributors/{fig}'.format(b=self.base_url, rpt=report_id, chp=chapter_id, fig=figure_id)

        data = {
            'delete': {
                'role': contrib.role.type_id,
                'organization_identifier': contrib.organization.identifier,
                'person_id': contrib.person.identifier
            }
        }

        return self.s.post(url, data=json.dumps(data), verify=False)

    @http_resp
    def associate_contributor_with_image(self, contrib, image_id):
        url = '{b}/image/contributors/{img}'.format(b=self.base_url, img=image_id)

        data = {
            'role': contrib.role.type_id,
        }
        if contrib.person is not None and contrib.person.id is not None:
            data['person_id'] = contrib.person.id
        if contrib.organization is not None and contrib.organization.identifier:
            data['organization_identifier'] = contrib.organization.identifier

        return self.s.post(url, data=json.dumps(data), verify=False)

    @http_resp
    def delete_contributor_image_assoc(self, contrib, image_id):
        url = '{b}/image/contributors/{img}'.format(b=self.base_url, img=image_id)

        data = {
            'delete': {
                'role': contrib.role.type_id,
                'organization_identifier': contrib.organization.identifier,
                'person_id': contrib.person.identifier
            }
        }
525

526
        return self.s.post(url, data=json.dumps(data), verify=False)
527