Completed
Push — devel ( 205479...086b96 )
by Paolo
21s queued 17s
created

pyUSIrest.usi.check_relationship()   A

Complexity

Conditions 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nop 2
dl 0
loc 18
rs 9.95
c 0
b 0
f 0
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
"""
4
Created on Thu May 24 16:41:31 2018
5
6
@author: Paolo Cozzi <[email protected]>
7
"""
8
9
import copy
10
import requests
11
import logging
12
import datetime
13
import collections
14
15
from url_normalize import url_normalize
16
17
from . import settings
18
from .client import Client, Document
19
from .exceptions import USIConnectionError, NotReadyError, USIDataError
20
21
22
logger = logging.getLogger(__name__)
23
24
25
class Root(Document):
26
    """Models the USI API Root_ endpoint
27
28
    Attributes:
29
        api_root (str): The base URL for API endpoints
30
31
    .. _Root: https://submission-test.ebi.ac.uk/api/docs/ref_root_endpoint.html
32
    """
33
34
    # define the default url
35
    api_root = None
36
37
    def __init__(self, auth):
38
        # calling the base class method client
39
        Client.__init__(self, auth)
40
        Document.__init__(self)
41
42
        # setting api_root
43
        self.api_root = settings.ROOT_URL + "/api/"
44
45
        # setting things. All stuff is inherithed
46
        self.get(self.api_root)
47
48
    def __str__(self):
49
        return "Biosample API root at %s" % (self.api_root)
50
51
    def get_user_teams(self):
52
        """Follow userTeams url and returns all teams belonging to user
53
54
        Yield:
55
            Team: a team object
56
        """
57
58
        # follow url
59
        document = self.follow_tag('userTeams')
60
61
        # check if I have submission
62
        if 'teams' not in document._embedded:
63
            logger.warning("You haven't any team yet!")
64
            return
65
66
        # now iterate over teams and create new objects
67
        for document in document.paginate():
68
            for team_data in document._embedded['teams']:
69
                team = Team(self.auth, team_data)
70
71
                logger.debug("Found %s team" % (team.name))
72
73
                # returning teams as generator
74
                yield team
75
76
    def get_team_by_name(self, team_name):
77
        """Get a :py:class:`Team` object by name
78
79
        Args:
80
            team_name (str): the name of the team
81
82
        Returns:
83
            Team: a team object
84
85
        """
86
        logger.debug("Searching for %s" % (team_name))
87
88
        for team in self.get_user_teams():
89
            if team.name == team_name:
90
                return team
91
92
        # if I arrive here, no team is found
93
        raise NameError("team: {team} not found".format(team=team_name))
94
95 View Code Duplication
    def get_user_submissions(self, status=None, team=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
96
        """Follow the userSubmission url and returns all submission owned by
97
        the user
98
99
        Args:
100
            status (str): filter user submissions using this status
101
            team (str): filter user submissions belonging to this team
102
103
        Returns:
104
            list: A list of :py:class:`Submission` objects
105
        """
106
107
        # follow url
108
        document = self.follow_tag('userSubmissions')
109
110
        # check if I have submission
111
        if 'submissions' not in document._embedded:
112
            logger.warning("You haven't any submission yet!")
113
            return
114
115
        # now iterate over submissions and create new objects
116
        for document in document.paginate():
117
            for submission_data in document._embedded['submissions']:
118
                submission = Submission(self.auth, submission_data)
119
120
                if status and submission.status != status:
121
                    logger.debug("Filtering %s submission" % (submission.name))
122
                    continue
123
124
                if team and submission.team != team:
125
                    logger.debug("Filtering %s submission" % (submission.name))
126
                    continue
127
128
                logger.debug("Found %s submission" % (submission.name))
129
130
                yield submission
131
132
    def get_submission_by_name(self, submission_name):
133
        """Got a specific submission object by providing its name
134
135
        Args:
136
            submission_name (str): input submission name
137
138
        Returns:
139
            Submission: The desidered submission as instance
140
        """
141
142
        # define submission url
143
        url = "/".join([self.api_root, 'submissions', submission_name])
144
145
        # fixing url (normalizing)
146
        url = url_normalize(url)
147
148
        # create a new submission object
149
        submission = Submission(self.auth)
150
151
        # doing a request
152
        try:
153
            submission.get(url)
154
155
        except USIConnectionError as exc:
156
            if submission.last_status_code == 404:
157
                # if I arrive here, no submission is found
158
                raise NameError(
159
                    "submission: '{name}' not found".format(
160
                        name=submission_name))
161
162
            else:
163
                raise exc
164
165
        return submission
166
167
168
class User(Document):
169
    """Deal with EBI AAP endpoint to get user information
170
171
    Attributes:
172
        name (str): Output of ``Auth.claims['nickname']``
173
        data (dict): data (dict): data from AAP read with
174
            :py:meth:`response.json() <requests.Response.json>`
175
        userName (str): AAP username
176
        email (str): AAP email
177
        userReference (str): AAP userReference
178
    """
179
180
    user_url = None
181
182
    def __init__(self, auth, data=None):
183
        """Instantiate the class
184
185
        Args:
186
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
187
            data (dict): instantiate the class from a dictionary of user data
188
        """
189
        # calling the base class method client
190
        Client.__init__(self, auth)
191
        Document.__init__(self)
192
193
        # define the base user url
194
        self.user_url = settings.AUTH_URL + "/users"
195
196
        # my class attributes
197
        self.name = self.auth.claims["nickname"]
198
        self.data = None
199
200
        # other attributes
201
        self.userName = None
202
        self.email = None
203
        self.userReference = None
204
        self.links = None
205
206
        # dealing with this type of documents.
207
        if data:
208
            logger.debug("Reading data for user")
209
            self.read_data(data)
210
211
    def get_my_id(self):
212
        """Get user id using own credentials, and set userReference attribute
213
214
        Returns:
215
            str: the user AAP reference as a string
216
        """
217
218
        # defining URL
219
        url = "%s/%s" % (self.user_url, self.name)
220
221
        logger.debug("Getting info from %s" % (url))
222
223
        # read url and get my attributes
224
        self.get(url)
225
226
        # returning user id
227
        return self.userReference
228
229
    def get_user_by_id(self, user_id):
230
        """Get a :py:class:`User` object by user_id
231
232
        Args:
233
            user_id (str): the required user_id
234
235
        Returns:
236
            User: a user object
237
        """
238
239
        # defining URL
240
        url = "%s/%s" % (self.user_url, user_id)
241
242
        logger.debug("Getting info from %s" % (url))
243
244
        # create a new user obj
245
        user = User(self.auth)
246
247
        # read url and get data
248
        user.get(url)
249
250
        # returning user
251
        return user
252
253
    @classmethod
254
    def create_user(cls, user, password, confirmPwd, email, full_name,
255
                    organisation):
256
        """Create another user into biosample AAP and return its ID
257
258
        Args:
259
            user (str): the new username
260
            password (str): the user password
261
            confirmPwd (str): the user confirm password
262
            email (str): the user email
263
            full_name (str): Full name of the user
264
            organisation (str): organisation name
265
266
        Returns:
267
            str: the new user_id as a string
268
        """
269
270
        # check that passwords are the same
271
        if password != confirmPwd:
272
            raise ValueError("passwords don't match!!!")
273
274
        # the AAP url
275
        url = settings.AUTH_URL + "/auth"
276
277
        # define a new header
278
        headers = {}
279
280
        # add new element to headers
281
        headers['Content-Type'] = 'application/json;charset=UTF-8'
282
283
        # TODO: use more informative parameters
284
        data = {
285
            "username": user,
286
            "password": password,
287
            "confirmPwd": confirmPwd,
288
            "email": email,
289
            "name": full_name,
290
            "organisation": organisation
291
        }
292
293
        # call a post method a deal with response. I don't need a client
294
        # object to create a new user
295
        session = requests.Session()
296
        response = session.post(url, json=data, headers=headers)
297
298
        if response.status_code != 200:
299
            raise USIConnectionError(response.text)
300
301
        # returning user id
302
        return response.text
303
304
    def create_team(self, description, centreName):
305
        """Create a new team
306
307
        Args:
308
            description (str): team description
309
            centreName (str): team center name
310
311
        Returns:
312
            Team: the new team as a :py:class:`Team` instance
313
        """
314
315
        url = settings.ROOT_URL + "/api/user/teams"
316
317
        # define a new header. Copy the dictionary, don't use the same object
318
        headers = copy.copy(self.headers)
319
320
        # add new element to headers
321
        headers['Content-Type'] = 'application/json;charset=UTF-8'
322
323
        data = {
324
            "description": description,
325
            "centreName": centreName
326
        }
327
328
        # call a post method a deal with response
329
        response = self.post(url, payload=data, headers=headers)
330
331
        # If I create a new team, the Auth object need to be updated
332
        logger.warning(
333
            "You need to generate a new token in order to see the new "
334
            "generated team")
335
336
        # create a new team object
337
        team = Team(self.auth, response.json())
338
339
        return team
340
341
    def get_teams(self):
342
        """Get teams belonging to this instance
343
344
        Returns:
345
            list: a list of :py:class:`Team` objects
346
        """
347
348
        url = settings.ROOT_URL + "/api/user/teams"
349
350
        # create a new document
351
        document = Document(auth=self.auth)
352
        document.get(url)
353
354
        # now iterate over teams and create new objects
355
        for document in document.paginate():
356
            for team_data in document._embedded['teams']:
357
                team = Team(self.auth, team_data)
358
                logger.debug("Found %s team" % (team.name))
359
360
                # returning teams as generator
361
                yield team
362
363
    def get_team_by_name(self, team_name):
364
        """Get a team by name
365
366
        Args:
367
            team_name (str): the required team
368
369
        Returns:
370
            Team: the desidered :py:class:`Team` instance
371
        """
372
373
        logger.debug("Searching for %s" % (team_name))
374
375
        for team in self.get_teams():
376
            if team.name == team_name:
377
                return team
378
379
        # if I arrive here, no team is found
380
        raise NameError("team: {team} not found".format(team=team_name))
381
382
    def get_domains(self):
383
        """Get domains belonging to this instance
384
385
        Returns:
386
            list: a list of :py:class:`Domain` objects
387
        """
388
389
        url = settings.AUTH_URL + "/my/domains"
390
391
        # make a request with a client object
392
        response = Client.get(self, url)
393
394
        # iterate over domains (they are a list of objects)
395
        for domain_data in response.json():
396
            domain = Domain(self.auth, domain_data)
397
            logger.debug("Found %s domain" % (domain.name))
398
399
            # returning domain as a generator
400
            yield domain
401
402
    def get_domain_by_name(self, domain_name):
403
        """Get a domain by name
404
405
        Args:
406
            domain_name (str): the required team
407
408
        Returns:
409
            Domain: the desidered :py:class:`Domain` instance
410
        """
411
412
        logger.debug("Searching for %s" % (domain_name))
413
414
        # get all domains
415
        for domain in self.get_domains():
416
            if domain.domainName == domain_name:
417
                return domain
418
419
        # if I arrive here, no team is found
420
        raise NameError("domain: {domain} not found".format(
421
            domain=domain_name))
422
423
    def add_user_to_team(self, user_id, domain_id):
424
        """Add a user to a team
425
426
        Args:
427
            user_id (str): the required user_id
428
            domain_id (str) the required domain_id
429
430
        Returns:
431
            Domain: the updated :py:class:`Domain` object"""
432
433
        url = (
434
            "{auth_url}/domains/{domain_id}/"
435
            "{user_id}/user".format(
436
                domain_id=domain_id,
437
                user_id=user_id,
438
                auth_url=settings.AUTH_URL)
439
        )
440
441
        response = self.put(url)
442
        domain_data = response.json()
443
444
        return Domain(self.auth, domain_data)
445
446
447
class Domain(Document):
448
    """
449
    A class to deal with AAP domain objects
450
451
    Attributes:
452
        name (str): domain name
453
        data (dict): data (dict): data from AAP read with
454
            :py:meth:`response.json() <requests.Response.json>`
455
        domainName (str): AAP domainName
456
        domainDesc (str): AAP domainDesc
457
        domainReference (str): AAP domainReference
458
        link (dict): ``links`` data read from AAP response
459
    """
460
461
    def __init__(self, auth, data=None):
462
        """Instantiate the class
463
464
        Args:
465
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
466
            data (dict): instantiate the class from a dictionary of user data
467
        """
468
469
        # calling the base class method client
470
        Client.__init__(self, auth)
471
        Document.__init__(self)
472
473
        # my class attributes
474
        self.data = None
475
476
        # other attributes
477
        self.domainName = None
478
        self.domainDesc = None
479
        self.domainReference = None
480
        self.links = None
481
        self._users = None
482
483
        # dealing with this type of documents.
484
        if data:
485
            logger.debug("Reading data for team")
486
            self.read_data(data)
487
488
            # this class lacks of a name attribute, so
489
            self.name = self.domainName
490
491
    def __str__(self):
492
        if not self.domainReference:
493
            return "domain not yet initialized"
494
495
        reference = self.domainReference.split("-")[1]
496
        return "%s %s %s" % (reference, self.name, self.domainDesc)
497
498
    @property
499
    def users(self):
500
        """Get users belonging to this domain"""
501
502
        if not self._users and isinstance(self.links, list):
503
            for url in self.links:
504
                if 'user' in url['href']:
505
                    # using the base get method
506
                    response = Client.get(self, url['href'])
507
                    break
508
509
            tmp_data = response.json()
0 ignored issues
show
introduced by
The variable response does not seem to be defined for all execution paths.
Loading history...
510
511
            # parse users as User objects
512
            self._users = []
513
514
            for user_data in tmp_data:
515
                self._users.append(User(self.auth, data=user_data))
516
517
        return self._users
518
519
    @users.setter
520
    def users(self, value):
521
        self._users = value
522
523
    def create_profile(self, attributes={}):
524
        """Create a profile for this domain
525
526
        Args:
527
            attributes (dict): a dictionary of attributes
528
        """
529
530
        # see this url for more information
531
        # https://explore.api.aai.ebi.ac.uk/docs/profile/index.html#resource-create_domain_profile
532
        url = settings.AUTH_URL + "/profiles"
533
534
        # define a new header. Copy the dictionary, don't use the same object
535
        headers = copy.copy(self.headers)
536
537
        # add new element to headers
538
        headers['Content-Type'] = 'application/json;charset=UTF-8'
539
540
        # define data
541
        data = {
542
            "domain": {
543
                "domainReference": self.domainReference
544
            },
545
            "attributes": attributes
546
        }
547
548
        # call a post method a deal with response
549
        response = self.post(url, payload=data, headers=headers)
550
551
        # create a new domain object
552
        domain = Domain(self.auth, response.json())
553
554
        return domain
555
556
557
class Team(Document):
558
    """A class to deal with USI Team_ objects
559
560
    Attributes:
561
        name (str): team name
562
        data (dict): data (dict): data from USI read with
563
            :py:meth:`response.json() <requests.Response.json>`
564
565
    .. _Team: https://submission-test.ebi.ac.uk/api/docs/ref_teams.html
566
    """
567
    def __init__(self, auth, data=None):
568
        """Instantiate the class
569
570
        Args:
571
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
572
            data (dict): instantiate the class from a dictionary of user data
573
        """
574
575
        # calling the base class method client
576
        Client.__init__(self, auth)
577
        Document.__init__(self)
578
579
        # my class attributes
580
        self.name = None
581
        self.data = None
582
        self.description = None
583
        self.profile = None
584
585
        # dealing with this type of documents.
586
        if data:
587
            logger.debug("Reading data for team")
588
            self.read_data(data)
589
590
    def __str__(self):
591
        return "{0} ({1})".format(self.name, self.description)
592
593 View Code Duplication
    def get_submissions(self, status=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
594
        """Follows submission url and get submissions from this team
595
596
        Args:
597
            status (str): filter submission using status
598
599
        Returns:
600
            list: A list of :py:class:`Submission` objects"""
601
602
        # follow url
603
        document = self.follow_tag('submissions')
604
605
        # check if I have submission
606
        if 'submissions' not in document._embedded:
607
            logger.warning("You haven't any submission yet!")
608
            return
609
610
        # now iterate over submissions and create new objects
611
        for document in document.paginate():
612
            for submission_data in document._embedded['submissions']:
613
                submission = Submission(self.auth, submission_data)
614
615
                if status and submission.status != status:
616
                    logger.debug("Filtering %s submission" % (submission.name))
617
                    continue
618
619
                logger.debug("Found %s submission" % (submission.name))
620
621
                yield submission
622
623
    def create_submission(self):
624
        """Create a new submission
625
626
        Returns:
627
            Submission: the new submission as an instance"""
628
629
        # get the url for submission:create. I don't want a document using
630
        # get method, I need instead a POST request
631
        url = self._links['submissions:create']['href']
632
633
        # define a new header. Copy the dictionary, don't use the same object
634
        headers = copy.copy(self.headers)
635
636
        # add new element to headers
637
        headers['Content-Type'] = 'application/json;charset=UTF-8'
638
639
        # call a post method a deal with response
640
        response = self.post(url, payload={}, headers=headers)
641
642
        # create a new Submission object
643
        submission = Submission(auth=self.auth, data=response.json())
644
645
        # there are some difference between a new submission and
646
        # an already defined submission
647
        logger.debug("reload submission to fix issues")
648
649
        # calling this method will reload submission and its status
650
        submission.reload()
651
652
        return submission
653
654
655
# helper functions
656
def check_relationship(sample_data, team):
657
    """Check relationship and add additional fields if missing"""
658
659
    # create a copy of sample_data
660
    sample_data = copy.copy(sample_data)
661
662
    # check relationship if exists
663
    if 'sampleRelationships' not in sample_data:
664
        return sample_data
665
666
    for relationship in sample_data['sampleRelationships']:
667
        if 'team' not in relationship:
668
            logger.debug("Adding %s to relationship" % (team))
669
            # setting the referenced object
670
            relationship['team'] = team
671
672
    # this is the copied sample_data, not the original one!!!
673
    return sample_data
674
675
676
def check_releasedate(sample_data):
677
    """Add release date to sample data if missing"""
678
679
    # create a copy of sample_data
680
    sample_data = copy.copy(sample_data)
681
682
    # add a default release date if missing
683
    if 'releaseDate' not in sample_data:
684
        today = datetime.date.today()
685
        logger.warning("Adding %s as releasedate")
686
        sample_data['releaseDate'] = str(today)
687
688
    # this is the copied sample_data, not the original one!!!
689
    return sample_data
690
691
692
class Submission(Document):
693
    """A class to deal with USI Submissions_
694
695
    Attributes:
696
        id (str): submission id (:py:meth:`~name` for compatibility)
697
        createdDate (str): created date
698
        lastModifiedDate (str): last modified date
699
        lastModifiedBy (str): last user_id who modified this submission
700
        submissionStatus (str): submission status
701
        submitter (dict): submitter data
702
        createdBy (str):  user_id who create this submission
703
        submissionDate (str): date when this submission is submitted to
704
            biosample
705
706
    .. _Submissions: https://submission-test.ebi.ac.uk/api/docs/ref_submissions.html
707
    """  # noqa
708
709
    def __init__(self, auth, data=None):
710
        """Instantiate the class
711
712
        Args:
713
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
714
            data (dict): instantiate the class from a dictionary of user data
715
        """
716
717
        # this will track submission name
718
        self.id = None
719
720
        # calling the base class method client
721
        Client.__init__(self, auth)
722
        Document.__init__(self)
723
724
        # my class attributes
725
        self._team = None
726
        self.createdDate = None
727
        self.lastModifiedDate = None
728
        self.lastModifiedBy = None
729
        self.submissionStatus = None
730
        self.submitter = None
731
        self.createdBy = None
732
733
        # when this attribute appears? maybe when submission take place
734
        self.submissionDate = None
735
736
        # each document need to parse data as dictionary, since there could be
737
        # more submission read from the same page. I cant read data from
738
        # self.last_response itself, cause I can't have a last response
739
        if data:
740
            self.read_data(data)
741
742
    def __str__(self):
743
        if not self.name:
744
            return "Submission not yet initialized"
745
746
        return "%s %s %s %s" % (
747
            self.name,
748
            self.team,
749
            self.lastModifiedDate.date(),
750
            self.status,)
751
752
    # for compatibility
753
    @property
754
    def name(self):
755
        """Get/Set Submission :py:attr:`~id`"""
756
757
        return self.id
758
759
    @name.setter
760
    def name(self, submission_id):
761
        if submission_id != self.id:
762
            logger.debug(
763
                    "Overriding id (%s > %s)" % (self.id, submission_id))
764
765
        self.id = submission_id
766
767
    @property
768
    def team(self):
769
        """Get/Set team name"""
770
771
        # get team name
772
        if isinstance(self._team, str):
773
            team_name = self._team
774
775
        elif isinstance(self._team, dict):
776
            team_name = self._team['name']
777
778
        elif self._team is None:
779
            team_name = ""
780
781
        else:
782
            raise NotImplementedError(
783
                "Unknown type: %s" % type(self._team)
784
            )
785
786
        return team_name
787
788
    @team.setter
789
    def team(self, value):
790
        self._team = value
791
792
    def read_data(self, data, force_keys=False):
793
        """Read data from a dictionary object and set class attributes
794
795
        Args:
796
            data (dict): a data dictionary object read with
797
                :py:meth:`response.json() <requests.Response.json>`
798
            force_keys (bool): If True, define a new class attribute from data
799
                keys
800
        """
801
802
        logger.debug("Reading data for submission")
803
        super().read_data(data, force_keys)
804
805
        # check for name
806
        if 'self' in self._links:
807
            name = self._links['self']['href'].split("/")[-1]
808
809
            # remove {?projection} name
810
            if '{?projection}' in name:
811
                logger.debug("removing {?projection} from name")
812
                name = name.replace("{?projection}", "")
813
814
            logger.debug("Using %s as submission name" % (name))
815
            self.name = name
816
817
    def check_ready(self):
818
        """Test if a submission can be submitted or not (Must have completed
819
        validation processes)
820
821
        Returns:
822
            bool: True if ready for submission
823
        """
824
825
        # Try to determine url manually
826
        url = (
827
            "{api_root}/api/submissions/"
828
            "{submission_name}/availableSubmissionStatuses".format(
829
                submission_name=self.name,
830
                api_root=settings.ROOT_URL)
831
        )
832
833
        # read a url in a new docume nt
834
        document = Document.read_url(self.auth, url)
835
836
        if hasattr(document, "_embedded"):
837
            if 'statusDescriptions' in document._embedded:
838
                return True
839
840
        # default response
841
        return False
842
843
    @property
844
    def status(self):
845
        """Return :py:attr:`~submissionStatus` attribute. Follow
846
        ``submissionStatus`` link and update attribute is such attribute is
847
        None
848
849
        Returns:
850
            str: submission status as a string"""
851
852
        if self.submissionStatus is None:
853
            self.update_status()
854
855
        return self.submissionStatus
856
857
    def update_status(self):
858
        """Update :py:attr:`~submissionStatus` attribute by following
859
        ``submissionStatus`` link"""
860
861
        document = self.follow_tag('submissionStatus')
862
        self.submissionStatus = document.status
863
864
    def create_sample(self, sample_data):
865
        """Create a sample from a dictionary
866
867
        Args:
868
            sample_data (dict): a dictionary of data
869
870
        Returns:
871
            Sample: a :py:class:`Sample` object"""
872
873
        # check sample_data for required attributes
874
        sample_data = check_relationship(sample_data, self.team)
875
        sample_data = check_releasedate(sample_data)
876
877
        # debug
878
        logger.debug(sample_data)
879
880
        # check if submission has the contents key
881
        if 'contents' not in self._links:
882
            # reload submission object in order to add items to it
883
            self.reload()
884
885
        # get the url for sample create
886
        document = self.follow_tag("contents")
887
888
        # get the url for submission:create. I don't want a document using
889
        # get method, I need instead a POST request
890
        url = document._links['samples:create']['href']
891
892
        # define a new header. Copy the dictionary, don't use the same object
893
        headers = copy.copy(self.headers)
894
895
        # add new element to headers
896
        headers['Content-Type'] = 'application/json;charset=UTF-8'
897
898
        # call a post method a deal with response
899
        response = self.post(url, payload=sample_data, headers=headers)
900
901
        # create a new sample
902
        sample = Sample(auth=self.auth, data=response.json())
903
904
        # returning sample as and object
905
        return sample
906
907
    def get_samples(self, status=None, has_errors=None,
908
                    ignorelist=[]):
909
        """Returning all samples as a list. Can filter by errors and error
910
        types::
911
912
            # returning samples with errors in other checks than Ena
913
            submission.get_samples(has_errors=True, ignorelist=['Ena'])
914
915
            # returning samples which validation is still in progress
916
            submission.get_samples(status='Pending')
917
918
        Get all sample with errors in other fields than *Ena* databank
919
920
        Args:
921
            status (str): filter samples by validation status
922
                (Pending, Complete)
923
            has_errors (bool): filter samples with errors or none
924
            ingnore_list (list): a list of errors to ignore
925
926
        Yield:
927
            Sample: a :py:class:`Sample` object
928
        """
929
930
        # get sample url in one step
931
        self_url = self._links['self']['href']
932
        samples_url = "/".join([self_url, "contents/samples"])
933
934
        # read a new document
935
        document = Document.read_url(self.auth, samples_url)
936
937
        # empty submission hasn't '_embedded' key
938
        if '_embedded' not in document.data:
939
            logger.warning("You haven't any samples yet!")
940
            return
941
942
        # now iterate over samples and create new objects
943
        for document in document.paginate():
944
            for sample_data in document._embedded['samples']:
945
                sample = Sample(self.auth, sample_data)
946
947
                if (status and
948
                        sample.get_validation_result().validationStatus
949
                        != status):
950
                    logger.debug("Filtering %s sample" % (sample))
951
                    continue
952
953
                if has_errors and has_errors != sample.has_errors(ignorelist):
954
                    logger.debug("Filtering %s sample" % (sample))
955
                    continue
956
957
                logger.debug("Found %s sample" % (sample))
958
959
                yield sample
960
961
    def get_validation_results(self):
962
        """Return validation results for submission
963
964
        Yield:
965
            ValidationResult: a :py:class:`ValidationResult` object"""
966
967
        # deal with different subission instances
968
        if 'validationResults' not in self._links:
969
            logger.warning("reloading submission")
970
            self.reload()
971
972
        document = self.follow_tag('validationResults')
973
974
        # now iterate over validationresults and create new objects
975
        for document in document.paginate():
976
            for validation_data in document._embedded['validationResults']:
977
                validation_result = ValidationResult(
978
                    self.auth, validation_data)
979
980
                logger.debug("Found %s sample" % (validation_result))
981
982
                yield validation_result
983
984
    def get_status(self):
985
        """Count validation statues for submission
986
987
        Returns:
988
            collections.Counter: A counter object for different validation
989
            status"""
990
991
        # get validation results
992
        validations = self.get_validation_results()
993
994
        # get statuses
995
        statuses = [validation.validationStatus for validation in validations]
996
997
        return collections.Counter(statuses)
998
999
    # there are errors that could be ignored
1000
    def has_errors(self, ignorelist=[]):
1001
        """Count sample errors for a submission
1002
1003
        Args:
1004
            ignorelist (list): ignore samples with errors in these databanks
1005
1006
        Returns:
1007
            collections.Counter: A counter object for samples with errors and
1008
            with no errors"""
1009
1010
        # check errors only if validation is completed
1011
        if 'Pending' in self.get_status():
1012
            raise NotReadyError(
1013
                "You can check errors after validation is completed")
1014
1015
        # get validation results
1016
        validations = self.get_validation_results()
1017
1018
        # get errors
1019
        errors = [
1020
            validation.has_errors(ignorelist) for validation in validations]
1021
1022
        return collections.Counter(errors)
1023
1024
    def delete(self):
1025
        """Delete this submission instance from USI"""
1026
1027
        url = self._links['self:delete']['href']
1028
        logger.info("Removing submission %s" % self.name)
1029
1030
        # don't return anything
1031
        Client.delete(self, url)
1032
1033
    def reload(self):
1034
        """call :py:meth:`Document.follow_self_url` and reload class
1035
        attributes"""
1036
1037
        logger.info("Refreshing data data for submission")
1038
        self.follow_self_url()
1039
1040
        # reload submission status
1041
        self.update_status()
1042
1043
    def finalize(self, ignorelist=[]):
1044
        """Finalize a submission to insert data into biosample
1045
1046
        Args:
1047
            ignorelist (list): ignore samples with errors in these databanks
1048
1049
        Returns:
1050
            Document: output of finalize submission as a :py:class:`Document`
1051
            object
1052
        """
1053
1054
        if not self.check_ready():
1055
            raise NotReadyError("Submission not ready for finalization")
1056
1057
        # raise exception if submission has errors
1058
        if True in self.has_errors(ignorelist):
1059
            raise USIDataError("Submission has errors, fix them")
1060
1061
        # refresh my data
1062
        self.reload()
1063
1064
        document = self.follow_tag('submissionStatus')
1065
1066
        # get the url to change
1067
        url = document._links['submissionStatus']['href']
1068
1069
        # define a new header. Copy the dictionary, don't use the same object
1070
        headers = copy.copy(self.headers)
1071
1072
        # add new element to headers
1073
        headers['Content-Type'] = 'application/json;charset=UTF-8'
1074
1075
        response = self.put(
1076
            url,
1077
            payload={'status': 'Submitted'},
1078
            headers=headers)
1079
1080
        # create a new document
1081
        document = Document(auth=self.auth, data=response.json())
1082
1083
        # copying last responsponse in order to improve data assignment
1084
        logger.debug("Assigning %s to document" % (response))
1085
1086
        document.last_response = response
1087
        document.last_status_code = response.status_code
1088
1089
        # update submission status
1090
        self.update_status()
1091
1092
        return document
1093
1094
1095
class Sample(Document):
1096
    """A class to deal with USI Samples_
1097
1098
    Attributes:
1099
        alias (str): The sample alias (used to reference the same object)
1100
        team (dict): team data
1101
        title (str): sample title
1102
        description (str): sample description
1103
        attributes (dict): sample attributes
1104
        sampleRelationships (list): relationship between samples
1105
        taxonId (int): taxon id
1106
        taxon (str): taxon name
1107
        releaseDate (str): when this sample will be relased to public
1108
        createdDate (str): created date
1109
        lastModifiedDate (str): last modified date
1110
        createdBy (str):  user_id who create this sample
1111
        lastModifiedBy (str): last user_id who modified this sample
1112
        accession (str): the biosample_id after submission to USI
1113
1114
    .. _Samples: https://submission-test.ebi.ac.uk/api/docs/ref_samples.html
1115
    """
1116
1117
    def __init__(self, auth, data=None):
1118
        """Instantiate the class
1119
1120
        Args:
1121
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
1122
            data (dict): instantiate the class from a dictionary of user data
1123
        """
1124
1125
        # calling the base class method client
1126
        Client.__init__(self, auth)
1127
        Document.__init__(self)
1128
1129
        # my class attributes
1130
        self.alias = None
1131
        self.team = None
1132
        self.title = None
1133
        self.description = None
1134
        self.attributes = None
1135
        self.sampleRelationships = None
1136
        self.taxonId = None
1137
        self.taxon = None
1138
        self.releaseDate = None
1139
        self.createdDate = None
1140
        self.lastModifiedDate = None
1141
        self.createdBy = None
1142
        self.lastModifiedBy = None
1143
1144
        # when this attribute appears? maybe when submission take place
1145
        self.accession = None
1146
1147
        if data:
1148
            self.read_data(data)
1149
1150
    def __str__(self):
1151
        # get accession or alias
1152
        if self.accession:
1153
            return "%s (%s)" % (self.accession, self.title)
1154
        else:
1155
            return "%s (%s)" % (self.alias, self.title)
1156
1157
    def read_data(self, data, force_keys=False):
1158
        """Read data from a dictionary object and set class attributes
1159
1160
        Args:
1161
            data (dict): a data dictionary object read with
1162
                :py:meth:`response.json() <requests.Response.json>`
1163
            force_keys (bool): If True, define a new class attribute from data
1164
                keys
1165
        """
1166
1167
        logger.debug("Reading data for Sample")
1168
        super().read_data(data, force_keys)
1169
1170
        # check for name
1171
        if 'self' in self._links:
1172
            self.name = self._links['self']['href'].split("/")[-1]
1173
            logger.debug("Using %s as sample name" % (self.name))
1174
1175
    def delete(self):
1176
        """Delete this instance from a submission"""
1177
1178
        url = self._links['self:delete']['href']
1179
        logger.info("Removing sample %s from submission" % self.name)
1180
1181
        response = Client.delete(self, url)
1182
1183
        if response.status_code != 204:
1184
            raise USIConnectionError(response.text)
1185
1186
        # assign attributes
1187
        self.last_response = response
1188
        self.last_status_code = response.status_code
1189
1190
        # don't return anything
1191
1192
    def reload(self):
1193
        """call :py:meth:`Document.follow_self_url` and reload class
1194
        attributes"""
1195
1196
        logger.info("Refreshing data data for sample")
1197
        self.follow_self_url()
1198
1199
    def patch(self, sample_data):
1200
        """Update sample by patching data with :py:meth:`Client.patch`
1201
1202
        Args:
1203
            sample_data (dict): sample data to update"""
1204
1205
        # check sample_data for required attributes
1206
        sample_data = check_relationship(sample_data, self.team)
1207
        sample_data = check_releasedate(sample_data)
1208
1209
        url = self._links['self']['href']
1210
        logger.info("patching sample %s with %s" % (self.name, sample_data))
1211
1212
        Client.patch(self, url, payload=sample_data)
1213
1214
        # reloading data
1215
        self.reload()
1216
1217
    def get_validation_result(self):
1218
        """Return validation results for submission
1219
1220
        Returns:
1221
            ValidationResult: the :py:class:`ValidationResult` of this sample
1222
        """
1223
1224
        document = self.follow_tag('validationResult', force_keys=True)
1225
1226
        return ValidationResult(self.auth, document.data)
1227
1228
    # there are errors that could be ignored
1229
    def has_errors(self, ignorelist=[]):
1230
        """Return True if validation results throw an error
1231
1232
        Args:
1233
            ignorelist (list): ignore errors in these databanks
1234
1235
        Returns:
1236
            bool: True if sample has an errors in one or more databank"""
1237
1238
        validation = self.get_validation_result().has_errors(ignorelist)
1239
1240
        if validation:
1241
            logger.error("Got error(s) for %s" % (self))
1242
1243
        return validation
1244
1245
1246
class ValidationResult(Document):
1247
    def __init__(self, auth, data=None):
1248
        """Instantiate the class
1249
1250
        Args:
1251
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
1252
            data (dict): instantiate the class from a dictionary of user data
1253
        """
1254
1255
        # calling the base class method client
1256
        Client.__init__(self, auth)
1257
        Document.__init__(self)
1258
1259
        # my class attributes
1260
        self.version = None
1261
        self.expectedResults = None
1262
        self.errorMessages = None
1263
        self.overallValidationOutcomeByAuthor = None
1264
        self.validationStatus = None
1265
1266
        if data:
1267
            self.read_data(data)
1268
1269
    def __str__(self):
1270
        message = self.validationStatus
1271
1272
        if self.overallValidationOutcomeByAuthor:
1273
            message += " %s" % (str(self.overallValidationOutcomeByAuthor))
1274
1275
        return message
1276
1277
    # there are errors that could be ignored
1278
    def has_errors(self, ignorelist=[]):
1279
        """Return True if validation has errors
1280
1281
        Args:
1282
            ignorelist (list): ignore errors in these databanks
1283
1284
        Returns:
1285
            bool: True if sample has errors for at least one databank"""
1286
1287
        has_errors = False
1288
1289
        for key, value in self.overallValidationOutcomeByAuthor.items():
1290
            if value == 'Error' and key not in ignorelist:
1291
                message = ", ".join(self.errorMessages[key])
1292
                logger.error(message)
1293
                has_errors = True
1294
1295
        return has_errors
1296