gcis_client.py 20.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
    @http_resp
abuddenberg's avatar
abuddenberg committed
294
    def create_dataset(self, dataset):
295
        url = '{b}/dataset/'.format(b=self.base_url)
296
        return self.s.post(url, data=dataset.as_json(), verify=False)
297

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

abuddenberg's avatar
abuddenberg committed
303
    @http_resp
304
305
    def delete_dataset(self, dataset):
        url = '{b}/dataset/{ds}'.format(b=self.base_url, ds=dataset.identifier)
306
        return self.s.delete(url, verify=False)
307

abuddenberg's avatar
abuddenberg committed
308
309
310
311
312
    @http_resp
    def get_dataset_list(self):
        url = '{b}/dataset/'.format(b=self.base_url)
        return self.s.get(url, params={'all': 1}, verify=False)

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

316
317
318
319
        data = {
            'parent_uri': '/dataset/' + dataset_id,
            'parent_rel': 'prov:wasDerivedFrom'
        }
abuddenberg's avatar
abuddenberg committed
320
321
322
        if activity_id:
            data['activity'] = activity_id

323
324
325
326
327
        try:
            self.delete_dataset_image_assoc(dataset_id, image_id)
        except AssociationException as e:
            print e.value

328
        resp = self.s.post(url, data=json.dumps(data), verify=False)
329
330
331
332

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

abuddenberg's avatar
abuddenberg committed
335
336
337
338
339
340
341
342
343
    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'
            }
        }
344
        resp = self.s.post(url, data=json.dumps(data), verify=False)
abuddenberg's avatar
abuddenberg committed
345
346
347
348

        if resp.status_code == 200:
            return resp
        else:
349
350
351
352
353
354
            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
355
            self.update_dataset(dataset)
356
357
        else:
            print 'Creating dataset: ' + dataset.identifier
358
            self.create_dataset(dataset)
359

abuddenberg's avatar
abuddenberg committed
360
361
362
    # @exists
    def activity_exists(self, activity_id):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=activity_id)
363
        resp = self.s.head(url, verify=False)
abuddenberg's avatar
abuddenberg committed
364
365
366
367
368
369
370
        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)
371
        resp = self.s.get(url, verify=False)
abuddenberg's avatar
abuddenberg committed
372
373
374
        try:
            return Activity(resp.json())
        except ValueError:
abuddenberg's avatar
abuddenberg committed
375
            raise Exception(resp.text)
abuddenberg's avatar
abuddenberg committed
376
377
378
379

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

    @http_resp
383
384
    def update_activity(self, activity, old_id=None):
        url = '{b}/activity/{act}'.format(b=self.base_url, act=old_id or activity.identifier)
385
        return self.s.post(url, data=activity.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
386
387
388
389

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

392
393
394
395
396
    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
397

398
399
400
401
402
    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
403
404
405
    @exists
    def person_exists(self, person_id):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person_id)
406
        return self.s.head(url, verfiy=False)
abuddenberg's avatar
abuddenberg committed
407
408
409

    def get_person(self, person_id):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person_id)
410
        resp = self.s.get(url, verify=False)
abuddenberg's avatar
abuddenberg committed
411
412
413
414
415
416
417
        try:
            return Person(resp.json())
        except ValueError:
            raise Exception(resp.text)

    def lookup_person(self, name):
        url = '{b}/autocomplete'.format(b=self.base_url)
418
        resp = self.s.get(url, params={'q': name, 'items': 15, 'type': 'person'}, verify=False)
abuddenberg's avatar
abuddenberg committed
419
420
421
422
423
424
425
426
427

        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)
428
        return self.s.post(url, data=person.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
429
430

    @http_resp
431
432
    def update_person(self, person, old_id=None):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=old_id or person.identifier)
433
        return self.s.post(url, data=person.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
434
435
436
437

    @http_resp
    def delete_person(self, person):
        url = '{b}/person/{pid}'.format(b=self.base_url, pid=person.identifier)
438
        return self.s.delete(url, verify=False)
abuddenberg's avatar
abuddenberg committed
439
440
441
442

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

    def get_organization(self, org_id):
446
447
        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
448
449
450
451
452
453
454

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

    def lookup_organization(self, name):
455
        url = '{b}/autocomplete'.format(b=self.base_url)
456
        resp = self.s.get(url, params={'q': name, 'items': 15, 'type': 'organization'}, verify=False)
457
458
459
460
461
        
        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
462
463
464
465

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

    @http_resp
469
470
    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)
471
        return self.s.post(url, data=org.as_json(), verify=False)
abuddenberg's avatar
abuddenberg committed
472
473
474
475

    @http_resp
    def delete_organization(self, org):
        url = '{b}/organization/{org_id}'.format(b=self.base_url, org_id=org.identifier)
476
        return self.s.delete(url, verify=False)
477
478
479
480
481

    @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)

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
525
526
527
528
529
530
531
532
        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
            }
        }
533

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

abuddenberg's avatar
abuddenberg committed
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
    @http_resp
    def associate_figure_with_report(self, figure_id, report_id, other_report_id):
        url = '{b}/report/{rpt}/figure/prov/{fig}'.format(b=self.base_url, rpt=report_id, fig=figure_id)

        data = {
            'parent_uri': '/report/' + other_report_id,
            'parent_rel': 'prov:wasDerivedFrom'
        }

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

    def lookup_publication(self, pub_type, name):
        url = '{b}/autocomplete'.format(b=self.base_url)
        resp = self.s.get(url, params={'q': name, 'items': 15, 'type': pub_type}, verify=False)

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