pyUSIrest.usi.Sample.__str__()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 1
dl 0
loc 6
rs 10
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 USIDataError 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 TeamMixin(object):
693
694
    def __init__(self):
695
        """Instantiate the class"""
696
697
        # my class attributes
698
        self._team = None
699
700
    @property
701
    def team(self):
702
        """Get/Set team name"""
703
704
        # get team name
705
        if isinstance(self._team, str):
706
            team_name = self._team
707
708
        elif isinstance(self._team, dict):
709
            team_name = self._team['name']
710
711
        elif self._team is None:
712
            team_name = ""
713
714
        else:
715
            raise NotImplementedError(
716
                "Unknown type: %s" % type(self._team)
717
            )
718
719
        return team_name
720
721
    @team.setter
722
    def team(self, value):
723
        self._team = value
724
725
726
class Submission(TeamMixin, Document):
727
    """A class to deal with USI Submissions_
728
729
    Attributes:
730
        id (str): submission id (:py:meth:`~name` for compatibility)
731
        createdDate (str): created date
732
        lastModifiedDate (str): last modified date
733
        lastModifiedBy (str): last user_id who modified this submission
734
        submissionStatus (str): submission status
735
        submitter (dict): submitter data
736
        createdBy (str):  user_id who create this submission
737
        submissionDate (str): date when this submission is submitted to
738
            biosample
739
740
    .. _Submissions: https://submission-test.ebi.ac.uk/api/docs/ref_submissions.html
741
    """  # noqa
742
743
    def __init__(self, auth, data=None):
744
        """Instantiate the class
745
746
        Args:
747
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
748
            data (dict): instantiate the class from a dictionary of user data
749
        """
750
751
        # this will track submission name
752
        self.id = None
753
754
        # calling the base class method client
755
        super().__init__()
756
757
        # now setting up Client and document class attributes
758
        Client.__init__(self, auth)
759
        Document.__init__(self)
760
761
        # my class attributes
762
        self.createdDate = None
763
        self.lastModifiedDate = None
764
        self.lastModifiedBy = None
765
        self.submissionStatus = None
766
        self.submitter = None
767
        self.createdBy = None
768
769
        # when this attribute appears? maybe when submission take place
770
        self.submissionDate = None
771
772
        # each document need to parse data as dictionary, since there could be
773
        # more submission read from the same page. I cant read data from
774
        # self.last_response itself, cause I can't have a last response
775
        if data:
776
            self.read_data(data)
777
778
    def __str__(self):
779
        if not self.name:
780
            return "Submission not yet initialized"
781
782
        return "%s %s %s %s" % (
783
            self.name,
784
            self.team,
785
            self.lastModifiedDate.date(),
786
            self.status,)
787
788
    # for compatibility
789
    @property
790
    def name(self):
791
        """Get/Set Submission :py:attr:`~id`"""
792
793
        return self.id
794
795
    @name.setter
796
    def name(self, submission_id):
797
        if submission_id != self.id:
798
            logger.debug(
799
                    "Overriding id (%s > %s)" % (self.id, submission_id))
800
801
        self.id = submission_id
802
803
    def read_data(self, data, force_keys=False):
804
        """Read data from a dictionary object and set class attributes
805
806
        Args:
807
            data (dict): a data dictionary object read with
808
                :py:meth:`response.json() <requests.Response.json>`
809
            force_keys (bool): If True, define a new class attribute from data
810
                keys
811
        """
812
813
        logger.debug("Reading data for submission")
814
        super().read_data(data, force_keys)
815
816
        # check for name
817
        if 'self' in self._links:
818
            name = self._links['self']['href'].split("/")[-1]
819
820
            # remove {?projection} name
821
            if '{?projection}' in name:
822
                logger.debug("removing {?projection} from name")
823
                name = name.replace("{?projection}", "")
824
825
            logger.debug("Using %s as submission name" % (name))
826
            self.name = name
827
828
    def check_ready(self):
829
        """Test if a submission can be submitted or not (Must have completed
830
        validation processes)
831
832
        Returns:
833
            bool: True if ready for submission
834
        """
835
836
        # Try to determine url manually
837
        url = (
838
            "{api_root}/api/submissions/"
839
            "{submission_name}/availableSubmissionStatuses".format(
840
                submission_name=self.name,
841
                api_root=settings.ROOT_URL)
842
        )
843
844
        # read a url in a new docume nt
845
        document = Document.read_url(self.auth, url)
846
847
        if hasattr(document, "_embedded"):
848
            if 'statusDescriptions' in document._embedded:
849
                return True
850
851
        # default response
852
        return False
853
854
    @property
855
    def status(self):
856
        """Return :py:attr:`~submissionStatus` attribute. Follow
857
        ``submissionStatus`` link and update attribute is such attribute is
858
        None
859
860
        Returns:
861
            str: submission status as a string"""
862
863
        if self.submissionStatus is None:
864
            self.update_status()
865
866
        return self.submissionStatus
867
868
    def update_status(self):
869
        """Update :py:attr:`~submissionStatus` attribute by following
870
        ``submissionStatus`` link"""
871
872
        document = self.follow_tag('submissionStatus')
873
        self.submissionStatus = document.status
874
875
    def create_sample(self, sample_data):
876
        """Create a sample from a dictionary
877
878
        Args:
879
            sample_data (dict): a dictionary of data
880
881
        Returns:
882
            Sample: a :py:class:`Sample` object"""
883
884
        # check sample_data for required attributes
885
        fixed_data = check_relationship(sample_data, self.team)
886
        fixed_data = check_releasedate(fixed_data)
887
888
        # debug
889
        logger.debug(fixed_data)
890
891
        # check if submission has the contents key
892
        if 'contents' not in self._links:
893
            # reload submission object in order to add items to it
894
            self.reload()
895
896
        # get the url for sample create
897
        document = self.follow_tag("contents")
898
899
        # get the url for submission:create. I don't want a document using
900
        # get method, I need instead a POST request
901
        url = document._links['samples:create']['href']
902
903
        # define a new header. Copy the dictionary, don't use the same object
904
        headers = copy.copy(self.headers)
905
906
        # add new element to headers
907
        headers['Content-Type'] = 'application/json;charset=UTF-8'
908
909
        # call a post method a deal with response
910
        response = self.post(url, payload=fixed_data, headers=headers)
911
912
        # create a new sample
913
        sample = Sample(auth=self.auth, data=response.json())
914
915
        # returning sample as and object
916
        return sample
917
918
    def get_samples(self, status=None, has_errors=None,
919
                    ignorelist=[]):
920
        """Returning all samples as a list. Can filter by errors and error
921
        types::
922
923
            # returning samples with errors in other checks than Ena
924
            submission.get_samples(has_errors=True, ignorelist=['Ena'])
925
926
            # returning samples which validation is still in progress
927
            submission.get_samples(status='Pending')
928
929
        Get all sample with errors in other fields than *Ena* databank
930
931
        Args:
932
            status (str): filter samples by validation status
933
                (Pending, Complete)
934
            has_errors (bool): filter samples with errors or none
935
            ingnore_list (list): a list of errors to ignore
936
937
        Yield:
938
            Sample: a :py:class:`Sample` object
939
        """
940
941
        # get sample url in one step
942
        self_url = self._links['self']['href']
943
        samples_url = "/".join([self_url, "contents/samples"])
944
945
        # read a new document
946
        document = Document.read_url(self.auth, samples_url)
947
948
        # empty submission hasn't '_embedded' key
949
        if '_embedded' not in document.data:
950
            logger.warning("You haven't any samples yet!")
951
            return
952
953
        # now iterate over samples and create new objects
954
        for document in document.paginate():
955
            for sample_data in document._embedded['samples']:
956
                sample = Sample(self.auth, sample_data)
957
958
                if (status and
959
                        sample.get_validation_result().validationStatus
960
                        != status):
961
                    logger.debug("Filtering %s sample" % (sample))
962
                    continue
963
964
                if has_errors and has_errors != sample.has_errors(ignorelist):
965
                    logger.debug("Filtering %s sample" % (sample))
966
                    continue
967
968
                logger.debug("Found %s sample" % (sample))
969
970
                yield sample
971
972
    def get_validation_results(self):
973
        """Return validation results for submission
974
975
        Yield:
976
            ValidationResult: a :py:class:`ValidationResult` object"""
977
978
        # deal with different subission instances
979
        if 'validationResults' not in self._links:
980
            logger.warning("reloading submission")
981
            self.reload()
982
983
        document = self.follow_tag('validationResults')
984
985
        # now iterate over validationresults and create new objects
986
        for document in document.paginate():
987
            for validation_data in document._embedded['validationResults']:
988
                validation_result = ValidationResult(
989
                    self.auth, validation_data)
990
991
                logger.debug("Found %s sample" % (validation_result))
992
993
                yield validation_result
994
995
    def get_status(self):
996
        """Count validation statues for submission
997
998
        Returns:
999
            collections.Counter: A counter object for different validation
1000
            status"""
1001
1002
        # get validation results
1003
        validations = self.get_validation_results()
1004
1005
        # get statuses
1006
        statuses = [validation.validationStatus for validation in validations]
1007
1008
        return collections.Counter(statuses)
1009
1010
    # there are errors that could be ignored
1011
    def has_errors(self, ignorelist=[]):
1012
        """Count sample errors for a submission
1013
1014
        Args:
1015
            ignorelist (list): ignore samples with errors in these databanks
1016
1017
        Returns:
1018
            collections.Counter: A counter object for samples with errors and
1019
            with no errors"""
1020
1021
        # check errors only if validation is completed
1022
        if 'Pending' in self.get_status():
1023
            raise NotReadyError(
1024
                "You can check errors after validation is completed")
1025
1026
        # get validation results
1027
        validations = self.get_validation_results()
1028
1029
        # get errors
1030
        errors = [
1031
            validation.has_errors(ignorelist) for validation in validations]
1032
1033
        return collections.Counter(errors)
1034
1035
    def delete(self):
1036
        """Delete this submission instance from USI"""
1037
1038
        url = self._links['self:delete']['href']
1039
        logger.info("Removing submission %s" % self.name)
1040
1041
        # don't return anything
1042
        Client.delete(self, url)
1043
1044
    def reload(self):
1045
        """call :py:meth:`Document.follow_self_url` and reload class
1046
        attributes"""
1047
1048
        logger.info("Refreshing data data for submission")
1049
        self.follow_self_url()
1050
1051
        # reload submission status
1052
        self.update_status()
1053
1054
    def finalize(self, ignorelist=[]):
1055
        """Finalize a submission to insert data into biosample
1056
1057
        Args:
1058
            ignorelist (list): ignore samples with errors in these databanks
1059
1060
        Returns:
1061
            Document: output of finalize submission as a :py:class:`Document`
1062
            object
1063
        """
1064
1065
        if not self.check_ready():
1066
            raise NotReadyError("Submission not ready for finalization")
1067
1068
        # raise exception if submission has errors
1069
        if True in self.has_errors(ignorelist):
1070
            raise USIDataError("Submission has errors, fix them")
1071
1072
        # refresh my data
1073
        self.reload()
1074
1075
        document = self.follow_tag('submissionStatus')
1076
1077
        # get the url to change
1078
        url = document._links['submissionStatus']['href']
1079
1080
        # define a new header. Copy the dictionary, don't use the same object
1081
        headers = copy.copy(self.headers)
1082
1083
        # add new element to headers
1084
        headers['Content-Type'] = 'application/json;charset=UTF-8'
1085
1086
        response = self.put(
1087
            url,
1088
            payload={'status': 'Submitted'},
1089
            headers=headers)
1090
1091
        # create a new document
1092
        document = Document(auth=self.auth, data=response.json())
1093
1094
        # copying last responsponse in order to improve data assignment
1095
        logger.debug("Assigning %s to document" % (response))
1096
1097
        document.last_response = response
1098
        document.last_status_code = response.status_code
1099
1100
        # update submission status
1101
        self.update_status()
1102
1103
        return document
1104
1105
1106
class Sample(TeamMixin, Document):
1107
    """A class to deal with USI Samples_
1108
1109
    Attributes:
1110
        alias (str): The sample alias (used to reference the same object)
1111
        team (dict): team data
1112
        title (str): sample title
1113
        description (str): sample description
1114
        attributes (dict): sample attributes
1115
        sampleRelationships (list): relationship between samples
1116
        taxonId (int): taxon id
1117
        taxon (str): taxon name
1118
        releaseDate (str): when this sample will be relased to public
1119
        createdDate (str): created date
1120
        lastModifiedDate (str): last modified date
1121
        createdBy (str):  user_id who create this sample
1122
        lastModifiedBy (str): last user_id who modified this sample
1123
        accession (str): the biosample_id after submission to USI
1124
1125
    .. _Samples: https://submission-test.ebi.ac.uk/api/docs/ref_samples.html
1126
    """
1127
1128
    def __init__(self, auth, data=None):
1129
        """Instantiate the class
1130
1131
        Args:
1132
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
1133
            data (dict): instantiate the class from a dictionary of user data
1134
        """
1135
1136
        # calling the base class method client
1137
        super().__init__()
1138
1139
        # now setting up Client and document class attributes
1140
        Client.__init__(self, auth)
1141
        Document.__init__(self)
1142
1143
        # my class attributes
1144
        self.alias = None
1145
        self.team = None
1146
        self.title = None
1147
        self.description = None
1148
        self.attributes = None
1149
        self.sampleRelationships = None
1150
        self.taxonId = None
1151
        self.taxon = None
1152
        self.releaseDate = None
1153
        self.createdDate = None
1154
        self.lastModifiedDate = None
1155
        self.createdBy = None
1156
        self.lastModifiedBy = None
1157
1158
        # when this attribute appears? maybe when submission take place
1159
        self.accession = None
1160
1161
        if data:
1162
            self.read_data(data)
1163
1164
    def __str__(self):
1165
        # get accession or alias
1166
        if self.accession:
1167
            return "%s (%s)" % (self.accession, self.title)
1168
        else:
1169
            return "%s (%s)" % (self.alias, self.title)
1170
1171
    def read_data(self, data, force_keys=False):
1172
        """Read data from a dictionary object and set class attributes
1173
1174
        Args:
1175
            data (dict): a data dictionary object read with
1176
                :py:meth:`response.json() <requests.Response.json>`
1177
            force_keys (bool): If True, define a new class attribute from data
1178
                keys
1179
        """
1180
1181
        logger.debug("Reading data for Sample")
1182
        super().read_data(data, force_keys)
1183
1184
        # check for name
1185
        if 'self' in self._links:
1186
            self.name = self._links['self']['href'].split("/")[-1]
1187
            logger.debug("Using %s as sample name" % (self.name))
1188
1189
    def delete(self):
1190
        """Delete this instance from a submission"""
1191
1192
        url = self._links['self:delete']['href']
1193
        logger.info("Removing sample %s from submission" % self.name)
1194
1195
        # don't return anything
1196
        Client.delete(self, url)
1197
1198
    def reload(self):
1199
        """call :py:meth:`Document.follow_self_url` and reload class
1200
        attributes"""
1201
1202
        logger.info("Refreshing data data for sample")
1203
        self.follow_self_url()
1204
1205
    def patch(self, sample_data):
1206
        """Update sample by patching data with :py:meth:`Client.patch`
1207
1208
        Args:
1209
            sample_data (dict): sample data to update"""
1210
1211
        # check sample_data for required attributes
1212
        fixed_data = check_relationship(sample_data, self.team)
1213
        fixed_data = check_releasedate(fixed_data)
1214
1215
        url = self._links['self']['href']
1216
        logger.info("patching sample %s with %s" % (self.name, fixed_data))
1217
1218
        super().patch(url, payload=fixed_data)
1219
1220
        # reloading data
1221
        self.reload()
1222
1223
    def get_validation_result(self):
1224
        """Return validation results for submission
1225
1226
        Returns:
1227
            ValidationResult: the :py:class:`ValidationResult` of this sample
1228
        """
1229
1230
        document = self.follow_tag('validationResult', force_keys=True)
1231
1232
        return ValidationResult(self.auth, document.data)
1233
1234
    # there are errors that could be ignored
1235
    def has_errors(self, ignorelist=[]):
1236
        """Return True if validation results throw an error
1237
1238
        Args:
1239
            ignorelist (list): ignore errors in these databanks
1240
1241
        Returns:
1242
            bool: True if sample has an errors in one or more databank"""
1243
1244
        validation = self.get_validation_result().has_errors(ignorelist)
1245
1246
        if validation:
1247
            logger.error("Got error(s) for %s" % (self))
1248
1249
        return validation
1250
1251
1252
class ValidationResult(Document):
1253
    def __init__(self, auth, data=None):
1254
        """Instantiate the class
1255
1256
        Args:
1257
            auth (Auth): a valid :py:class:`Auth <pyUSIrest.auth.Auth>` object
1258
            data (dict): instantiate the class from a dictionary of user data
1259
        """
1260
1261
        # calling the base class method client
1262
        Client.__init__(self, auth)
1263
        Document.__init__(self)
1264
1265
        # my class attributes
1266
        self.version = None
1267
        self.expectedResults = None
1268
        self.errorMessages = None
1269
        self.overallValidationOutcomeByAuthor = None
1270
        self.validationStatus = None
1271
1272
        if data:
1273
            self.read_data(data)
1274
1275
    def __str__(self):
1276
        message = self.validationStatus
1277
1278
        if self.overallValidationOutcomeByAuthor:
1279
            message += " %s" % (str(self.overallValidationOutcomeByAuthor))
1280
1281
        return message
1282
1283
    # there are errors that could be ignored
1284
    def has_errors(self, ignorelist=[]):
1285
        """Return True if validation has errors
1286
1287
        Args:
1288
            ignorelist (list): ignore errors in these databanks
1289
1290
        Returns:
1291
            bool: True if sample has errors for at least one databank"""
1292
1293
        has_errors = False
1294
1295
        for key, value in self.overallValidationOutcomeByAuthor.items():
1296
            if value == 'Error' and key not in ignorelist:
1297
                message = ", ".join(self.errorMessages[key])
1298
                logger.error(message)
1299
                has_errors = True
1300
1301
        return has_errors
1302