~ubuntu-branches/ubuntu/trusty/qiime/trusty

« back to all changes in this revision

Viewing changes to .pc/make_qiime_accept_new_rdp_classifier/qiime/assign_taxonomy.py

  • Committer: Package Import Robot
  • Author(s): Andreas Tille
  • Date: 2013-06-17 18:28:26 UTC
  • mfrom: (9.1.2 sid)
  • Revision ID: package-import@ubuntu.com-20130617182826-376az5ad080a0sfe
Tags: 1.7.0+dfsg-1
Upload preparations done for BioLinux to Debian

Show diffs side-by-side

added added

removed removed

Lines of Context:
2
2
 
3
3
__author__ = "Rob Knight, Greg Caporaso"
4
4
__copyright__ = "Copyright 2011, The QIIME Project"
5
 
__credits__ = ["Rob Knight", "Greg Caporaso", "Kyle Bittinger", "Antonio Gonzalez Pena", "David Soergel"]
 
5
__credits__ = ["Rob Knight", "Greg Caporaso", "Kyle Bittinger",
 
6
               "Antonio Gonzalez Pena", "David Soergel", "Jai Ram Rideout"]
6
7
__license__ = "GPL"
7
 
__version__ = "1.5.0"
 
8
__version__ = "1.7.0"
8
9
__maintainer__ = "Greg Caporaso"
9
10
__email__ = "gregcaporaso@gmail.com"
10
11
__status__ = "Release"
11
12
 
12
 
#import csv
 
13
 
13
14
import logging
 
15
import os
14
16
import re
15
 
from os import system, remove, path, mkdir
16
 
from os.path import split, splitext
17
 
from glob import glob
 
17
from os import remove
18
18
from itertools import count
19
19
from string import strip
20
20
from shutil import copy as copy_file
21
21
from tempfile import NamedTemporaryFile
22
22
from cStringIO import StringIO
23
23
from cogent import LoadSeqs, DNA
24
 
from qiime.util import get_tmp_filename
25
24
from cogent.app.formatdb import build_blast_db_from_fasta_path
26
25
from cogent.app.blast import blast_seqs, Blastall, BlastResult
27
26
from qiime.pycogent_backports import rdp_classifier
28
 
from qiime.pycogent_backports import rtax
 
27
from cogent.app import rtax
 
28
from qiime.pycogent_backports import mothur
 
29
from cogent.app.util import ApplicationNotFoundError
29
30
from cogent.parse.fasta import MinimalFastaParser
30
 
from qiime.util import FunctionWithParams, get_rdp_jarpath
31
 
 
 
31
from qiime.util import FunctionWithParams, get_rdp_jarpath, get_qiime_temp_dir
 
32
 
 
33
# Load Tax2Tree if it's available. If it's not, skip it, but set up
 
34
# to raise errors if the user tries to use it.
 
35
try:
 
36
    from t2t.nlevel import load_consensus_map, load_tree, determine_rank_order
 
37
    from qiime.pycogent_backports import tax2tree
 
38
 
 
39
except ImportError:
 
40
    def raise_tax2tree_not_found_error(*args, **kwargs):
 
41
        raise ApplicationNotFoundError,\
 
42
         "Tax2Tree cannot be found.\nIs Tax2Tree installed? Is it in your $PYTHONPATH?"+\
 
43
         "\nYou can obtain Tax2Tree from http://sourceforge.net/projects/tax2tree/." 
 
44
    #set functions which cannot be imported to raise_tax2tree_not_found_error
 
45
    load_consensus = load_tree = determine_rank_order = tax2tree_controller = raise_tax2tree_not_found_error
32
46
 
33
47
"""Contains code for assigning taxonomy, using several techniques.
34
48
 
35
49
This module has the responsibility for taking a set of sequences and
36
50
providing a taxon assignment for each sequence."""
37
51
 
38
 
def guess_rdp_version(rdp_jarpath=None):
 
52
 
 
53
def validate_rdp_version(rdp_jarpath=None):
39
54
    if rdp_jarpath is None:
40
55
        rdp_jarpath = get_rdp_jarpath()
41
56
    if rdp_jarpath is None:
42
 
        raise ValueError(
43
 
            "RDP classifier is not installed or "
44
 
            "not accessible to QIIME. See install instructions here: "
45
 
            "http://qiime.org/install/install.html#rdp-install")
46
 
    elif "2.2" in rdp_jarpath:
47
 
        return "rdp22"
48
 
    else:
49
 
        raise ValueError(
50
 
            "RDP classifier filename does not look like version 2.2.  Only "
51
 
            "version 2.2 is supported by QIIME. RDP jar path is:"
52
 
            " %s" % rdp_jarpath)
 
57
        raise RuntimeError(
 
58
            "RDP classifier is not installed or not accessible to QIIME. "
 
59
            "See install instructions here: "
 
60
            "http://qiime.org/install/install.html#rdp-install"
 
61
            )
 
62
 
 
63
    rdp_jarname = os.path.basename(rdp_jarpath)
 
64
    version_match = re.search("\d\.\d", rdp_jarname)
 
65
    if version_match is None:
 
66
        raise RuntimeError(
 
67
            "Unable to detect RDP Classifier version in file %s" % rdp_jarname
 
68
            )
 
69
 
 
70
    version = float(version_match.group())
 
71
    if version < 2.1:
 
72
        raise RuntimeError(
 
73
            "RDP Classifier does not look like version 2.2 or greater."
 
74
            "Versions of the software prior to 2.2 have different "
 
75
            "formatting conventions and are no longer supported by QIIME. "
 
76
            "Detected version %s from file %s" % (version, rdp_jarpath)
 
77
            )
 
78
    return version
53
79
 
54
80
 
55
81
class TaxonAssigner(FunctionWithParams):
107
133
        """ Initialize the object
108
134
        """
109
135
        _params = {
110
 
            'Min percent identity': 0.90,
 
136
            'Min percent identity': 90.0,
111
137
            'Max E value': 1e-30,
112
138
            'Application': 'blastn/megablast'
113
139
            }
133
159
        except KeyError:
134
160
            # build a temporary blast_db
135
161
            reference_seqs_path = self.Params['reference_seqs_filepath']
136
 
            refseqs_dir, refseqs_name = split(reference_seqs_path)
 
162
            refseqs_dir, refseqs_name = os.path.split(reference_seqs_path)
137
163
            blast_db, db_files_to_remove = \
138
164
             build_blast_db_from_fasta_path(reference_seqs_path)
139
165
 
260
286
        """
261
287
        max_evalue = self.Params['Max E value']
262
288
        min_percent_identity = self.Params['Min percent identity']
 
289
        if min_percent_identity < 1.0:
 
290
            min_percent_identity *= 100.0
263
291
        seq_ids = [s[0] for s in seqs]
264
292
        result = {}
265
293
 
302
330
        return result
303
331
 
304
332
 
 
333
class MothurTaxonAssigner(TaxonAssigner):
 
334
    """Assign taxonomy using Mothur's naive Bayes implementation
 
335
    """
 
336
    Name = 'MothurTaxonAssigner'
 
337
    Application = "Mothur"
 
338
    Citation = (
 
339
        "Schloss, P.D., et al., Introducing mothur: Open-source, platform-"
 
340
        "independent, community-supported software for describing and "
 
341
        "comparing microbial communities. Appl Environ Microbiol, 2009. "
 
342
        "75(23):7537-41."
 
343
        )
 
344
    _tracked_properties = ['Application', 'Citation']
 
345
 
 
346
    def __init__(self, params):
 
347
        _params = {
 
348
            'Confidence': 0.80,
 
349
            'Iterations': None,
 
350
            'KmerSize': None,
 
351
            'id_to_taxonomy_fp': None,
 
352
            'reference_sequences_fp': None,
 
353
            }
 
354
        _params.update(params)
 
355
        super(MothurTaxonAssigner, self).__init__(_params)
 
356
 
 
357
    def __call__(self, seq_path, result_path=None, log_path=None):
 
358
        seq_file = open(seq_path)
 
359
        percent_confidence = int(self.Params['Confidence'] * 100)
 
360
        result = mothur.mothur_classify_file(
 
361
            query_file=seq_file,
 
362
            ref_fp=self.Params['reference_sequences_fp'],
 
363
            tax_fp=self.Params['id_to_taxonomy_fp'],
 
364
            cutoff=percent_confidence,
 
365
            iters=self.Params['Iterations'],
 
366
            ksize=self.Params['KmerSize'],
 
367
            output_fp=result_path,
 
368
            )
 
369
        if log_path:
 
370
            self.writeLog(log_path)
 
371
        return result
 
372
 
 
373
 
305
374
class RdpTaxonAssigner(TaxonAssigner):
306
375
    """Assign taxon using RDP's naive Bayesian classifier
307
376
    """
308
377
    Name = "RdpTaxonAssigner"
309
 
    Application = "RDP classfier, version 2.2"
 
378
    Application = "RDP classfier"
310
379
    Citation = "Wang, Q, G. M. Garrity, J. M. Tiedje, and J. R. Cole. 2007. Naive Bayesian Classifier for Rapid Assignment of rRNA Sequences into the New Bacterial Taxonomy. Appl Environ Microbiol. 73(16):5261-7."
311
380
    Taxonomy = "RDP"
312
381
    _tracked_properties = ['Application','Citation','Taxonomy']
328
397
        _params.update(params)
329
398
        TaxonAssigner.__init__(self, _params)
330
399
 
331
 
    @property
332
 
    def _assign_fcn(self):
333
 
        return rdp_classifier.assign_taxonomy
334
 
 
335
 
    @property
336
 
    def _train_fcn(self):
337
 
        return rdp_classifier.train_rdp_classifier_and_assign_taxonomy
338
 
 
339
400
    def __call__(self, seq_path, result_path=None, log_path=None):
340
401
        """Returns dict mapping {seq_id:(taxonomy, confidence)} for
341
402
        each seq.
346
407
            result to the desired path instead of returning it.
347
408
        log_path: path to log, which should include dump of params.
348
409
        """
349
 
 
 
410
        tmp_dir = get_qiime_temp_dir()
350
411
        min_conf = self.Params['Confidence']
351
412
        training_data_properties_fp = self.Params['training_data_properties_fp']
352
413
        reference_sequences_fp = self.Params['reference_sequences_fp']
353
414
        id_to_taxonomy_fp = self.Params['id_to_taxonomy_fp']
354
415
        max_memory = self.Params['max_memory']
355
416
 
356
 
        seq_file = open(seq_path, 'r')
 
417
        seq_file = open(seq_path, 'U')
357
418
        if reference_sequences_fp and id_to_taxonomy_fp:
358
419
            # Train and assign taxonomy
359
420
            taxonomy_file, training_seqs_file = self._generate_training_files()
360
 
            results = self._train_fcn(
 
421
            results = rdp_classifier.train_rdp_classifier_and_assign_taxonomy(
361
422
                training_seqs_file, taxonomy_file, seq_file,
362
423
                min_confidence=min_conf,
363
424
                classification_output_fp=result_path,
364
 
                max_memory=max_memory)
 
425
                max_memory=max_memory, tmp_dir=tmp_dir)
 
426
 
365
427
 
366
428
            if result_path is None:
367
429
                results = self._training_set.fix_results(results)
369
431
                self._training_set.fix_output_file(result_path)
370
432
        else:
371
433
            # Just assign taxonomy, using properties file if passed
372
 
            results = self._assign_fcn(
 
434
            if training_data_properties_fp:
 
435
                fix_ranks = False
 
436
            else:
 
437
                fix_ranks = True
 
438
            results = rdp_classifier.assign_taxonomy(
373
439
                seq_file, min_confidence=min_conf, output_fp=result_path,
374
440
                training_data_fp=training_data_properties_fp,
375
 
                max_memory=max_memory)
 
441
                max_memory=max_memory, fixrank=fix_ranks, tmp_dir=tmp_dir)
376
442
 
377
443
        if log_path:
378
444
            self.writeLog(log_path)
383
449
        """Returns a tuple of file objects suitable for passing to the
384
450
        RdpTrainer application controller.
385
451
        """
 
452
        tmp_dir = get_qiime_temp_dir()
386
453
        training_set = RdpTrainingSet()
387
 
        reference_seqs_file = open(self.Params['reference_sequences_fp'], 'r')
388
 
        id_to_taxonomy_file = open(self.Params['id_to_taxonomy_fp'], 'r')
 
454
        reference_seqs_file = open(self.Params['reference_sequences_fp'], 'U')
 
455
        id_to_taxonomy_file = open(self.Params['id_to_taxonomy_fp'], 'U')
389
456
 
390
457
        for seq_id, seq in MinimalFastaParser(reference_seqs_file):
391
458
            training_set.add_sequence(seq_id, seq)
397
464
        training_set.dereplicate_taxa()
398
465
 
399
466
        rdp_taxonomy_file = NamedTemporaryFile(
400
 
            prefix='RdpTaxonAssigner_taxonomy_', suffix='.txt')
 
467
            prefix='RdpTaxonAssigner_taxonomy_', suffix='.txt', dir=tmp_dir)
401
468
        rdp_taxonomy_file.write(training_set.get_rdp_taxonomy())
402
469
        rdp_taxonomy_file.seek(0)
403
470
 
404
471
        rdp_training_seqs_file = NamedTemporaryFile(
405
 
            prefix='RdpTaxonAssigner_training_seqs_', suffix='.fasta')
 
472
            prefix='RdpTaxonAssigner_training_seqs_', suffix='.fasta',
 
473
            dir=tmp_dir)
406
474
        for rdp_id, seq in training_set.get_training_seqs():
407
475
            rdp_training_seqs_file.write('>%s\n%s\n' % (rdp_id, seq))
408
476
        rdp_training_seqs_file.seek(0)
417
485
        self._tree = RdpTree()
418
486
        self.sequences = {}
419
487
        self.sequence_nodes = {}
 
488
        self.lineage_depth = None
420
489
 
421
490
    def add_sequence(self, seq_id, seq):
422
491
        self.sequences[seq_id] = seq
423
492
 
424
493
    def add_lineage(self, seq_id, lineage_str):
 
494
        for char, escape_str in _QIIME_RDP_ESCAPES:
 
495
            lineage_str = re.sub(char, escape_str, lineage_str)
425
496
        lineage = self._parse_lineage(lineage_str)
426
497
        seq_node = self._tree.insert_lineage(lineage)
427
498
        self.sequence_nodes[seq_id] = seq_node
434
505
        lineage string of an id_to_taxonomy file.
435
506
        """
436
507
        lineage = lineage_str.strip().split(';')
437
 
        if len(lineage) != 6:
 
508
        if self.lineage_depth is None:
 
509
            self.lineage_depth = len(lineage)
 
510
        if len(lineage) != self.lineage_depth:
438
511
            raise ValueError(
439
 
                'Each reference assignment must contain 6 items, specifying '
440
 
                'domain, phylum, class, order, family, and genus.  '
441
 
                'Detected %s items in "%s": %s.' % \
442
 
                (len(lineage), lineage_str, lineage))
 
512
                'Because the RDP Classifier operates in a bottom-up manner, '
 
513
                'each taxonomy assignment in the id-to-taxonomy file must have '
 
514
                'the same number of ranks.  Detected %s ranks in the first '
 
515
                'item of the file, but detected %s ranks later in the file. '
 
516
                'Offending taxonomy string: %s' %
 
517
                (self.lineage_depth, len(lineage), lineage_str))
443
518
        return lineage
444
519
 
445
520
    def get_training_seqs(self):
469
544
        # Ultimate hack to replace mangled taxa names
470
545
        temp_results = StringIO()
471
546
        for line in open(result_path):
472
 
            untagged_line = re.sub(
 
547
            line = re.sub(
473
548
                _QIIME_RDP_TAXON_TAG + "[^;\n\t]*", '', line)
474
 
            temp_results.write(untagged_line)
 
549
            for char, escape_str in _QIIME_RDP_ESCAPES:
 
550
                line = re.sub(escape_str, char, line)
 
551
            temp_results.write(line)
475
552
        open(result_path, 'w').write(temp_results.getvalue())
476
553
 
477
554
    def fix_results(self, results_dict):
478
555
        for seq_id, assignment in results_dict.iteritems():
479
556
            lineage, confidence = assignment
480
 
            revised_lineage = re.sub(
 
557
            lineage = re.sub(
481
558
                _QIIME_RDP_TAXON_TAG + "[^;\n\t]*", '', lineage)
482
 
            results_dict[seq_id] = (revised_lineage, confidence)
 
559
            for char, escape_str in _QIIME_RDP_ESCAPES:
 
560
                lineage = re.sub(escape_str, char, lineage)
 
561
            results_dict[seq_id] = (lineage, confidence)
483
562
        return results_dict
484
563
 
485
564
 
487
566
    """Simple, specialized tree class used to generate a taxonomy
488
567
    file for the Rdp Classifier.
489
568
    """
490
 
    taxonomic_ranks = [
491
 
        'norank', 'domain', 'phylum', 'class', 'order', 'family', 'genus']
 
569
    taxonomic_ranks = ' abcdefghijklmnopqrstuvwxyz'
492
570
 
493
571
    def __init__(self, name='Root', parent=None, counter=None):
494
572
        if counter is None:
536
614
                yield node
537
615
 
538
616
    def dereplicate_taxa(self):
 
617
        # We check that there are no duplicate taxon names (case insensitive)
 
618
        # at a given depth. We must do a case insensitive check because the RDP
 
619
        # classifier converts taxon names to lowercase when it checks for
 
620
        # duplicates, and will throw an error otherwise.
539
621
        taxa_by_depth = {}
540
622
        for node in self.get_nodes():
541
623
            name = node.name
542
624
            depth = node.depth
543
625
            current_names = taxa_by_depth.get(depth, set())
544
 
            if name in current_names:
 
626
            if name.lower() in current_names:
545
627
                node.name = name + _QIIME_RDP_TAXON_TAG + str(node.id)
546
628
            else:
547
 
                current_names.add(name)
 
629
                current_names.add(name.lower())
548
630
                taxa_by_depth[depth] = current_names
549
631
 
550
632
    def get_rdp_taxonomy(self):
556
638
        else:
557
639
            parent_id = self.parent.id
558
640
 
559
 
        rank_name = self.taxonomic_ranks[self.depth]
 
641
        # top rank name must be norank, and bottom rank must be genus
 
642
        if self.depth == 0:
 
643
            rank_name = "norank"
 
644
        elif self.children:
 
645
            rank_name = self.taxonomic_ranks[self.depth]
 
646
        else:
 
647
            rank_name = "genus"
560
648
 
561
649
        fields = [
562
650
            self.id, self.name, parent_id, self.depth, rank_name]
572
660
 
573
661
 
574
662
_QIIME_RDP_TAXON_TAG = "_qiime_unique_taxon_tag_"
 
663
_QIIME_RDP_ESCAPES = [
 
664
    ("&", "_qiime_ampersand_escape_"),
 
665
    (">", "_qiime_greaterthan_escape_"),
 
666
    ("<", "_qiime_lessthan_escape_"),
 
667
    ]
575
668
 
576
669
 
577
670
class RtaxTaxonAssigner(TaxonAssigner):
579
672
    """
580
673
    Name = "RtaxTaxonAssigner"
581
674
    Application = "RTAX classifier" # ", version 0.98"  # don't hardcode the version number, as it may change, and then the log output test would fail
582
 
    Citation = "Soergel D.A.W., Dey N., Knight R., and Brenner S.E.  2012.  Selection of primers for optimal taxonomic classification of environmental 16S rRNA gene sequences.  ISME J."
 
675
    Citation = "Soergel D.A.W., Dey N., Knight R., and Brenner S.E.  2012.  Selection of primers for optimal taxonomic classification of environmental 16S rRNA gene sequences.  ISME J (6), 1440-1444"
583
676
    _tracked_properties = ['Application','Citation']
584
677
 
585
678
    def __init__(self, params):
594
687
            'amplicon_id_regex' : "(\\S+)\\s+(\\S+?)\/",  # split_libraries produces >read_1_id ampliconID/1 .   This makes a map between read_1_id and ampliconID.
595
688
            'read_1_seqs_fp' : None,
596
689
            'read_2_seqs_fp' : None,
597
 
            'single_ok' : False
 
690
            'single_ok' : False,
 
691
            'no_single_ok_generic' : False
598
692
            }
599
693
        _params.update(params)
600
694
        TaxonAssigner.__init__(self, _params)
630
724
 
631
725
        read_2_seqs_fp=self.Params['read_2_seqs_fp']
632
726
        single_ok=self.Params['single_ok']
 
727
        no_single_ok_generic=self.Params['no_single_ok_generic']
633
728
        header_id_regex=self.Params['header_id_regex']
634
729
        assert header_id_regex, \
635
730
            "Must not provide empty header_id_regex when calling an RtaxTaxonAssigner; leave unset"\
639
734
        amplicon_id_regex=self.Params['amplicon_id_regex']
640
735
 
641
736
        # seq_file = open(seq_path, 'r')
 
737
 
642
738
        results = rtax.assign_taxonomy(seq_path, reference_sequences_fp, id_to_taxonomy_fp,
643
 
                                       read_1_seqs_fp, read_2_seqs_fp, single_ok=single_ok,
 
739
                                       read_1_seqs_fp, read_2_seqs_fp, single_ok=single_ok, no_single_ok_generic=no_single_ok_generic,
644
740
                                       header_id_regex=header_id_regex, read_id_regex=read_id_regex,
645
741
                                       amplicon_id_regex=amplicon_id_regex, output_fp=result_path,
646
 
                                       log_path=log_path)
647
 
 
648
 
 
649
 
        return results
 
742
                                       log_path=log_path,base_tmp_dir=get_qiime_temp_dir())
 
743
 
 
744
 
 
745
        return results
 
746
 
 
747
class Tax2TreeTaxonAssigner(TaxonAssigner):
 
748
    """Assign taxon using Tax2Tree
 
749
    """
 
750
    Name = "Tax2TreeTaxonAssigner"
 
751
    Application = "Tax2Tree"
 
752
    Citation = "Daniel McDonald"
 
753
    
 
754
    def __init__(self, params):
 
755
        """Returns a new Tax2TreeAssigner object with specified params
 
756
        """
 
757
        _params = {
 
758
            #Required. Used as consensus map.
 
759
            'id_to_taxonomy_fp': None,
 
760
            #Required. The aligned and filtered tree of combined input and reference seqs.
 
761
            'tree_fp': None,
 
762
            }
 
763
        _params.update(params)
 
764
        TaxonAssigner.__init__(self, _params)
 
765
        
 
766
    def __call__(self, seq_path=None, result_path=None, log_path=None):
 
767
        """Returns a dict mapping {seq_id:(taxonomy, confidence)} for each seq
 
768
 
 
769
        Keep in mind, "confidence" is only done for consistency and in fact
 
770
        all assignments will have a score of 0 because a method for determining
 
771
        confidence is not currently implemented.
 
772
 
 
773
        Parameters:
 
774
        seq_path: path to file of sequences. The sequences themselves are
 
775
            never actually used, but they are needed for their ids.
 
776
        result_path: path to file of results. If specified, dumps the
 
777
            result to the desired path instead of returning it.
 
778
        log_path: path to log, which should include dump of params.
 
779
        """
 
780
 
 
781
        # initialize the logger
 
782
        logger = self._get_logger(log_path)
 
783
        logger.info(str(self))
 
784
 
 
785
        with open(seq_path, 'U') as f:
 
786
            seqs = dict(MinimalFastaParser(f))
 
787
 
 
788
        consensus_map = tax2tree.prep_consensus(open(self.Params['id_to_taxonomy_fp']), seqs.keys())
 
789
        seed_con = consensus_map[0].strip().split('\t')[1]
 
790
        determine_rank_order(seed_con)
 
791
 
 
792
        tipnames_map = load_consensus_map(consensus_map, False)
 
793
 
 
794
        tree = load_tree(open(self.Params['tree_fp']), tipnames_map)
 
795
 
 
796
        results = tax2tree.generate_constrings(tree, tipnames_map)
 
797
        results = tax2tree.clean_output(results, seqs.keys())
 
798
 
 
799
        if result_path:
 
800
            # if the user provided a result_path, write the
 
801
            # results to file
 
802
            with open(result_path,'w') as f:
 
803
                for seq_id, (lineage, confidence) in results.iteritems():
 
804
                    f.write('%s\t%s\t%s\n' %(seq_id, lineage, confidence))
 
805
            logger.info('Result path: %s' % result_path)
 
806
            
 
807
 
 
808
        return results
 
809
 
 
810
    def _get_logger(self, log_path=None):
 
811
        if log_path is not None:
 
812
            handler = logging.FileHandler(log_path, mode='w')
 
813
        else:
 
814
            class NullHandler(logging.Handler):
 
815
                def emit(self, record): pass
 
816
            handler = NullHandler()
 
817
        logger = logging.getLogger("Tax2TreeTaxonAssigner logger")
 
818
        logger.addHandler(handler)
 
819
        logger.setLevel(logging.INFO)
 
820
        return logger