def get(self, table_id=""):
     """Checks if a given table exists"""
     con = db.get_engine(bind="summarization")
     if con.dialect.has_table(con, table_id):
         return BARUtils.success_exit(True)
     else:
         return BARUtils.success_exit(False)
 def get(self, table_id, sample, gene):
     """Returns the value for a given gene and sample. If no sample is given returns all values for that gene"""
     if not BARUtils.is_arabidopsis_gene_valid(gene):
         return BARUtils.success_exit("Invalid gene ID"), 400
     else:
         key = request.headers.get("X-Api-Key")
         if SummarizationGeneExpressionUtils.decrement_uses(key):
             con = db.get_engine(bind="summarization")
             tbl = SummarizationGeneExpressionUtils.get_table_object(
                 table_id)
             if sample == "":
                 values = {}
                 try:
                     rows = con.execute(
                         tbl.select(tbl.c.Value).where(tbl.c.Gene == gene))
                 except SQLAlchemyError:
                     return BARUtils.error_exit(
                         "Internal server error"), 500
                 for row in rows:
                     values.update({str(row.Sample): float(row.Value)})
             else:
                 values = []
                 try:
                     rows = con.execute(
                         tbl.select(tbl.c.Value).where(
                             tbl.c.Sample == sample).where(
                                 tbl.c.Gene == gene))
                 except SQLAlchemyError:
                     return BARUtils.error_exit(
                         "Internal server error"), 500
                 [values.append(row.Value) for row in rows]
             return BARUtils.success_exit(values)
         else:
             return BARUtils.error_exit("Invalid API key")
Example #3
0
 def post(self):
     """Verify admin password"""
     if request.method == "POST":
         response_json = request.get_json()
         password = response_json["password"]
         if ApiManagerUtils.check_admin_pass(password):
             return BARUtils.success_exit(True)
         else:
             return BARUtils.success_exit(False)
Example #4
0
 def post(self):
     """Verify admin password
     """
     if request.method == 'POST':
         response_json = request.get_json()
         user_key = response_json['key']
         if ApiManagerUtils.check_admin_pass(user_key):
             return BARUtils.success_exit(True)
         else:
             return BARUtils.success_exit(False)
Example #5
0
 def post(self):
     """Returns list of pending requests from the database
     """
     if request.method == 'POST':
         response_json = request.get_json()
         password = response_json['password']
         if ApiManagerUtils.check_admin_pass(password):
             table = Requests()
             values = []
             try:
                 rows = table.query.filter_by().all()
             except SQLAlchemyError:
                 return BARUtils.error_exit('Internal server error'), 500
             [
                 values.append({
                     'first_name': row.first_name,
                     'last_name': row.last_name,
                     'email': row.email,
                     'telephone': row.telephone,
                     'contact_type': row.contact_type,
                     'notes': row.notes
                 }) for row in rows
             ]
             return BARUtils.success_exit(values)
         else:
             return BARUtils.error_exit('Forbidden'), 403
    def post(self):
        """This end point returns gene expression data for a single gene and multiple samples."""
        json_data = request.get_json()

        # Validate json
        try:
            json_data = RNASeqSchema().load(json_data)
        except ValidationError as err:
            return BARUtils.error_exit(err.messages), 400

        species = json_data['species']
        database = json_data['database']
        gene_id = json_data['gene_id']
        sample_ids = json_data['sample_ids']

        results = RNASeqUtils.get_data(species, database, gene_id, sample_ids)

        if results['success']:
            # Return results if there are data
            if len(results['data']) > 0:
                return BARUtils.success_exit(results['data'])
            else:
                return BARUtils.error_exit(
                    'There are no data found for the given gene')
        else:
            return BARUtils.error_exit(results['error']), results['error_code']
 def post(self):
     """Takes a Google Drive folder ID (containing BAM files) and submits them to the Cromwell server for summarization
     """
     if request.method == 'POST':
         json = request.get_json()
         key = request.headers.get('X-Api-Key')
         if SummarizationGeneExpressionUtils.decrement_uses(key):
             inputs = """
                     {
                     'geneSummarization.gtf': './data/Araport11_GFF3_genes_transposons.201606.gtf',
                     'geneSummarization.summarizeGenesScript': './summarize_genes.R',
                     'geneSummarization.downloadFilesScript': './downloadDriveFiles.py',
                     'geneSummarization.insertDataScript': './insertData.py',
                     'geneSummarization.credentials': './data/credentials.json',
                     'geneSummarization.token': './data/token.pickle',
                     'geneSummarization.aliases': './data/aliases.txt',
                     'geneSummarization.folderId': """ + json[
                 'folderId'] + """,
                     'geneSummarization.id': """ + key + """
                     }
                     """
             # Create DB
             # Send request to Cromwell
             path = os.path.join(SUMMARIZATION_FILES_PATH, 'rpkm.wdl')
             files = {
                 'workflowSource': ('rpkm.wdl', open(path, 'rb')),
                 'workflowInputs': ('rpkm_inputs.json', inputs)
             }
             requests.post(CROMWELL_URL + '/api/workflows/v1', files=files)
             # Return ID for future accessing
             return BARUtils.success_exit(key), 200
         else:
             return BARUtils.error_exit('Invalid API key')
 def post(self):
     """Saves the given file if the user has a valid API key
     """
     if request.method == 'POST':
         api_key = request.headers.get('x-api-key')
         if api_key is None:
             return BARUtils.error_exit('Invalid API key'), 403
         elif SummarizationGeneExpressionUtils.decrement_uses(api_key):
             now = datetime.now()
             dt_string = now.strftime("%d-%m-%Y_%H-%M-%S")
             if 'file' in request.files:
                 file = request.files['file']
                 extension = ''
                 if file.content_type == 'text/xml':
                     extension = '.xml'
                 elif file.content_type == 'image/svg+xml':
                     extension = '.svg'
                 else:
                     return BARUtils.error_exit('Invalid file type'), 400
                 filename = os.path.join(DATA_FOLDER, api_key,
                                         dt_string + extension)
                 file.save(filename)
                 return BARUtils.success_exit(True)
             else:
                 return BARUtils.error_exit('No file attached'), 400
         else:
             return BARUtils.error_exit('Invalid API key')
 def post(self):
     """Takes a CSV file containing expression data and inserts the data into the database
     """
     if request.method == 'POST':
         if 'file' not in request.files:
             return BARUtils.error_exit('No file attached'), 400
         file = request.files['file']
         if file:
             filename = secure_filename(file.filename)
             key = request.headers.get('X-Api-Key')
             file.save(os.path.join(DATA_FOLDER + '/' + key + '/',
                                    filename))
             if SummarizationGeneExpressionUtils.decrement_uses(key):
                 inputs = """
                         {
                         'csvUpload.insertDataScript': './insertData.py',
                         'csvUpload.id': """ + key + """,
                         'csvUpload.csv': """ + os.path.join(
                     DATA_FOLDER, key, filename) + """,
                         }
                         """
                 path = os.path.join(SUMMARIZATION_FILES_PATH,
                                     'csvUpload.wdl')
                 files = {
                     'workflowSource': ('csvUpload.wdl', open(path, 'rb')),
                     'workflowInputs': ('rpkm_inputs.json', inputs)
                 }
                 requests.post(CROMWELL_URL + '/api/workflows/v1',
                               files=files)
                 return BARUtils.success_exit(key)
             else:
                 return BARUtils.error_exit('Invalid API key')
 def post(self):
     """Takes a CSV file containing expression data and inserts the data into the database"""
     if request.method == "POST":
         if "file" not in request.files:
             return BARUtils.error_exit("No file attached"), 400
         file = request.files["file"]
         if file:
             filename = secure_filename(file.filename)
             key = request.headers.get("X-Api-Key")
             file.save(os.path.join(DATA_FOLDER, key, filename))
             if SummarizationGeneExpressionUtils.decrement_uses(key):
                 inputs = ("""
                         {
                         "csvUpload.insertDataScript": "./insertData.py",
                         "csvUpload.id": """ + key + """,
                         "csvUpload.csv": """ +
                           os.path.join(DATA_FOLDER, key, filename) + """,
                         }
                         """)
                 path = os.path.join(SUMMARIZATION_FILES_PATH,
                                     "csvUpload.wdl")
                 files = {
                     "workflowSource": ("csvUpload.wdl", open(path, "rb")),
                     "workflowInputs": ("rpkm_inputs.json", inputs),
                 }
                 requests.post(CROMWELL_URL + "/api/workflows/v1",
                               files=files)
                 return BARUtils.success_exit(key)
             else:
                 return BARUtils.error_exit("Invalid API key")
Example #11
0
    def get(self, species='', gene_id=''):
        """This end point provides gene alias given a gene ID."""
        aliases = []

        # Escape input
        species = escape(species)
        gene_id = escape(gene_id)

        if species == 'arabidopsis':
            if BARUtils.is_arabidopsis_gene_valid(gene_id):
                try:
                    rows = AgiAlias.query.filter_by(agi=gene_id).all()
                except OperationalError:
                    return BARUtils.error_exit(
                        'An internal error has occurred'), 500
                [aliases.append(row.alias) for row in rows]
            else:
                return BARUtils.error_exit('Invalid gene id'), 400
        else:
            return BARUtils.error_exit('No data for the given species')

        # Return results if there are data
        if len(aliases) > 0:
            return BARUtils.success_exit(aliases)
        else:
            return BARUtils.error_exit(
                'There are no data found for the given gene')
Example #12
0
 def post(self):
     """Returns list of pending requests from the database"""
     if request.method == "POST":
         response_json = request.get_json()
         password = response_json["password"]
         if ApiManagerUtils.check_admin_pass(password):
             table = Requests()
             values = []
             try:
                 rows = table.query.filter_by().all()
             except SQLAlchemyError:
                 return BARUtils.error_exit("Internal server error"), 500
             [
                 values.append({
                     "first_name": row.first_name,
                     "last_name": row.last_name,
                     "email": row.email,
                     "telephone": row.telephone,
                     "contact_type": row.contact_type,
                     "notes": row.notes,
                 }) for row in rows
             ]
             return BARUtils.success_exit(values)
         else:
             return BARUtils.error_exit("Forbidden"), 403
 def post(self):
     """Returns a list of files stored in the user's folder
     """
     if request.method == 'POST':
         api_key = request.headers.get('x-api-key')
         files = []
         for file in os.walk(DATA_FOLDER + api_key):
             files.append(file[2])
         return BARUtils.success_exit(files)
 def post(self):
     """Returns a list of files stored in the user's folder"""
     if request.method == "POST":
         api_key = request.headers.get("x-api-key")
         files = []
         if os.path.exists(os.path.join(DATA_FOLDER, api_key)):
             for file in os.walk(os.path.join(DATA_FOLDER, api_key)):
                 files.append(file[2])
         return BARUtils.success_exit(files)
Example #15
0
    def get(self, species='', gene_id=''):
        """This end point provides gene isoforms given a gene ID.
        Only genes/isoforms with pdb structures are returned"""
        gene_isoforms = []

        # Escape input
        species = escape(species)
        gene_id = escape(gene_id)

        if species == 'arabidopsis':
            if BARUtils.is_arabidopsis_gene_valid(gene_id):
                try:
                    rows = isoforms.query.filter_by(gene=gene_id).all()
                except OperationalError:
                    return BARUtils.error_exit(
                        'An internal error has occurred'), 500
                [gene_isoforms.append(row.isoform) for row in rows]

                # Found isoforms
                if len(gene_isoforms) > 0:
                    return BARUtils.success_exit(gene_isoforms)
            else:
                return BARUtils.error_exit('Invalid gene id'), 400
        elif species == 'poplar':
            if BARUtils.is_poplar_gene_valid(gene_id):
                # Path is the location of poplar pdb file
                if os.environ.get('BAR'):
                    path = '/DATA/ePlants_Data/eplant_poplar/protein_structures/'
                else:
                    path = os.getcwd(
                    ) + '/data/gene_information/gene_isoforms/'

                path += gene_id + '.pdb'
                if os.path.exists(path) and os.path.isfile(path):
                    return BARUtils.success_exit(gene_id)
            else:
                return BARUtils.error_exit('Invalid gene id'), 400
        else:
            return BARUtils.error_exit('No data for the given species')

        return BARUtils.error_exit(
            'There are no data found for the given gene')
Example #16
0
    def post(self):
        """Verify if an API key provided by the user exists in the database
        """
        if request.method == 'POST':
            tbl = Users()
            json = request.get_json()
            key = json['key']
            try:
                row = tbl.query.filter_by(api_key=key).first()
            except SQLAlchemyError:
                return BARUtils.error_exit('Internal server error'), 500

            # Todo: I guess this is work in progress.
            if row is None:
                return BARUtils.success_exit('Do data found')
            else:
                if row.uses_left > 0:
                    return BARUtils.success_exit('True')
                else:
                    return BARUtils.success_exit('False')
 def get(self, table_id=""):
     """Returns the list of samples in the table with the given ID"""
     con = db.get_engine(bind="summarization")
     tbl = SummarizationGeneExpressionUtils.get_table_object(table_id)
     values = []
     try:
         rows = con.execute(db.select([tbl.c.Sample]).distinct())
     except SQLAlchemyError:
         return BARUtils.error_exit("Internal server error"), 500
     [values.append(row.Sample) for row in rows]
     return BARUtils.success_exit(values)
Example #18
0
    def get(self, gene_id=''):
        """ Endpoint returns annotated SNP poplar data in order of (to match A th API format):
            AA pos (zero-indexed), sample id, 'missense_variant','MODERATE', 'MISSENSE', codon/DNA base change,
            AA change (DH), pro length, gene ID, 'protein_coding', 'CODING', transcript id, biotype
            values with single quotes are fixed """
        results_json = []

        # Escape input
        gene_id = escape(gene_id)

        if BARUtils.is_poplar_gene_valid(gene_id) is False:
            return BARUtils.error_exit('Invalid gene id'), 400

        try:
            rows = db.session.query(ProteinReference, SnpsToProtein, SnpsReference). \
                select_from(ProteinReference). \
                join(SnpsToProtein). \
                join(SnpsReference). \
                filter(ProteinReference.gene_identifier == gene_id).all()

            # BAR A Th API format is chr, AA pos (zero-indexed), sample id, 'missense_variant',
            # 'MODERATE', 'MISSENSE', codon/DNA base change, AA change (DH),
            # pro length, gene ID, 'protein_coding', 'CODING', transcript id, biotype
            for protein, snpsjoin, snpstbl in rows:
                itm_lst = [
                    snpstbl.chromosome,
                    # snpstbl.chromosomal_loci,
                    snpsjoin.aa_pos - 1,  # zero index-ed
                    snpstbl.sample_id,
                    'missense_variant',
                    'MODERATE',
                    'MISSENSE',
                    str(snpsjoin.transcript_pos) + snpsjoin.ref_DNA + '>' +
                    snpsjoin.alt_DNA,
                    snpsjoin.ref_aa + snpsjoin.alt_aa,
                    None,
                    re.sub(r".\d$", '', protein.gene_identifier),
                    'protein_coding',
                    'CODING',
                    protein.gene_identifier,
                    None,
                ]
                results_json.append(itm_lst)
        except OperationalError:
            return BARUtils.error_exit('An internal error has occurred'), 500

        # Return results if there are data
        if len(results_json) > 0:
            return BARUtils.success_exit(results_json)
        else:
            return BARUtils.error_exit(
                'There are no data found for the given gene')
 def get(self, table_id="", user_string=""):
     """Returns all genes that contain a given string as part of their name"""
     con = db.get_engine(bind="summarization")
     tbl = SummarizationGeneExpressionUtils.get_table_object(table_id)
     values = []
     try:
         rows = con.execute(
             db.select([tbl.c.Gene]).where(
                 tbl.c.Gene.contains(user_string)).distinct())
     except SQLAlchemyError:
         return BARUtils.error_exit("Internal server error"), 500
     [values.append(row.Gene) for row in rows]
     return BARUtils.success_exit(values)
Example #20
0
    def post(self):
        if request.method == "POST":
            response_json = request.get_json()
            df = pandas.DataFrame.from_records([response_json])
            con = db.get_engine(bind="summarization")
            try:
                reqs = Requests()
                users = Users()
                row_req = reqs.query.filter_by(email=df.email[0]).first()
                row_users = users.query.filter_by(email=df.email[0]).first()

                if row_req is None and row_users is None:
                    df.to_sql("requests", con, if_exists="append", index=False)
                    if ApiManagerUtils.send_email():
                        return BARUtils.success_exit("Data added, email sent")
                    else:
                        return BARUtils.success_exit(
                            "Data added, email failed")
                else:
                    return BARUtils.error_exit("E-mail already in use"), 409
            except SQLAlchemyError:
                return BARUtils.error_exit("Internal server error"), 500
 def get(self, table_id=""):
     """Returns the list of genes in the table with the given ID"""
     key = request.headers.get("x-api-key")
     if SummarizationGeneExpressionUtils.decrement_uses(key):
         con = db.get_engine(bind="summarization")
         tbl = SummarizationGeneExpressionUtils.get_table_object(table_id)
         values = []
         try:
             rows = con.execute(db.select([tbl.c.Gene]).distinct())
         except SQLAlchemyError:
             return BARUtils.error_exit("Internal server error"), 500
         [values.append(row.Gene) for row in rows]
         return BARUtils.success_exit(values)
     else:
         return BARUtils.error_exit("Invalid API key")
Example #22
0
    def get(self, species="", gene_id=""):
        """This end point provides gene isoforms given a gene ID.
        Only genes/isoforms with pdb structures are returned"""
        gene_isoforms = []

        # Escape input
        species = escape(species)
        gene_id = escape(gene_id)

        # Set the database and check if genes are valid
        if species == "arabidopsis":
            database = eplant2_isoforms()

            if not BARUtils.is_arabidopsis_gene_valid(gene_id):
                return BARUtils.error_exit("Invalid gene id"), 400

        elif species == "poplar":
            database = eplant_poplar_isoforms

            if not BARUtils.is_poplar_gene_valid(gene_id):
                return BARUtils.error_exit("Invalid gene id"), 400

            # Format the gene first
            gene_id = BARUtils.format_poplar(gene_id)

        elif species == "tomato":
            database = eplant_tomato_isoforms

            if not BARUtils.is_tomato_gene_valid(gene_id, False):
                return BARUtils.error_exit("Invalid gene id"), 400
        else:
            return BARUtils.error_exit("No data for the given species")

        # Now get the data
        try:
            rows = database.query.filter_by(gene=gene_id).all()
        except OperationalError:
            return BARUtils.error_exit("An internal error has occurred"), 500
        [gene_isoforms.append(row.isoform) for row in rows]

        # Found isoforms
        if len(gene_isoforms) > 0:
            return BARUtils.success_exit(gene_isoforms)
        else:
            return BARUtils.error_exit(
                "There are no data found for the given gene")
Example #23
0
    def get(self, species="", database="", gene_id=""):
        """This end point returns RNA-Seq gene expression data"""
        # Variables
        species = escape(species)
        database = escape(database)
        gene_id = escape(gene_id)

        results = RNASeqUtils.get_data(species, database, gene_id)

        if results["success"]:
            # Return results if there are data
            if len(results["data"]) > 0:
                return BARUtils.success_exit(results["data"])
            else:
                return BARUtils.error_exit(
                    "There are no data found for the given gene")
        else:
            return BARUtils.error_exit(results["error"]), results["error_code"]
Example #24
0
 def post(self):
     """Validates a reCaptcha value using our secret token"""
     if request.method == "POST":
         json = request.get_json()
         value = json["response"]
         key = os.environ.get("CAPTCHA_KEY")
         if key:
             ret = requests.post(
                 "https://www.google.com/recaptcha/api/siteverify",
                 data={
                     "secret": key,
                     "response": value
                 },
             )
             return BARUtils.success_exit(ret.text)
         else:
             return BARUtils.error_exit(
                 "Forbidden: CAPTCHA key is not found"), 403
Example #25
0
    def post(self):
        """Verify if an API key provided by the user exists in the database"""
        if request.method == "POST":
            tbl = Users()
            json = request.get_json()
            key = json["key"]
            try:
                row = tbl.query.filter_by(api_key=key).first()
            except SQLAlchemyError:
                return BARUtils.error_exit("Internal server error"), 500

            if row is None:
                return BARUtils.error_exit("API key not found"), 404
            else:
                if row.uses_left > 0:
                    return BARUtils.success_exit(True)
                else:
                    return BARUtils.error_exit("API key expired"), 401
    def get(self, species='', database='', gene_id=''):
        """This end point returns RNA-Seq gene expression data"""
        # Variables
        species = escape(species)
        database = escape(database)
        gene_id = escape(gene_id)

        results = RNASeqUtils.get_data(species, database, gene_id)

        if results['success']:
            # Return results if there are data
            if len(results['data']) > 0:
                return BARUtils.success_exit(results['data'])
            else:
                return BARUtils.error_exit(
                    'There are no data found for the given gene')
        else:
            return BARUtils.error_exit(results['error']), results['error_code']
Example #27
0
 def post(self):
     """Validates a reCaptcha value using our secret token
     """
     if request.method == 'POST':
         json = request.get_json()
         value = json['response']
         key = os.environ.get('CAPTCHA_KEY')
         if key:
             ret = requests.post(
                 'https://www.google.com/recaptcha/api/siteverify',
                 data={
                     'secret': key,
                     'response': value
                 })
             return BARUtils.success_exit(ret.text)
         else:
             return BARUtils.error_exit(
                 'Forbidden: CAPTCHA key is not found'), 403
 def post(self):
     """Takes a Google Drive folder ID (containing BAM files) and submits them to the Cromwell server for summarization"""
     if request.method == "POST":
         json = request.get_json()
         key = request.headers.get("X-Api-Key")
         species = json["species"]
         email = json["email"]
         aliases = json["aliases"]
         gtf = GTF_DICT[species]
         if SummarizationGeneExpressionUtils.decrement_uses(key):
             inputs = {
                 "geneSummarization.summarizeGenesScript":
                 "./summarize_genes.R",
                 "geneSummarization.downloadFilesScript":
                 "./downloadDriveFiles.py",
                 "geneSummarization.chrsScript": "./chrs.py",
                 "geneSummarization.folderId": json["folderId"],
                 "geneSummarization.credentials": "./data/credentials.json",
                 "geneSummarization.token": "./data/token.pickle",
                 "geneSummarization.species": species,
                 "geneSummarization.gtf": gtf,
                 "geneSummarization.aliases": str(aliases),
                 "geneSummarization.id": key,
                 "geneSummarization.pairedEndScript": "./paired.sh",
                 "geneSummarization.insertDataScript": "./insertData.py",
                 "geneSummarization.barEmailScript": "./bar_email.py",
                 "geneSummarization.errorEmailScript": "./error_email.py",
                 "geneSummarization.email": email,
             }
             # Send request to Cromwell
             path = os.path.join(SUMMARIZATION_FILES_PATH, "rpkm.wdl")
             file = tempfile.TemporaryFile(mode="w+")
             file.write(jsonlib.dumps(inputs))
             file.seek(0)
             files = {
                 "workflowSource": ("rpkm.wdl", open(path, "rb")),
                 "workflowInputs": ("rpkm_inputs.json", file.read()),
             }
             requests.post(CROMWELL_URL + "/api/workflows/v1", files=files)
             file.close()
             # Return ID for future accessing
             return BARUtils.success_exit(key), 200
         else:
             return BARUtils.error_exit("Invalid API key")
Example #29
0
 def post(self):
     """Delete a request from the database"""
     if request.method == "POST":
         response_json = request.get_json()
         password = response_json["password"]
         if ApiManagerUtils.check_admin_pass(password):
             response_json = request.get_json()
             table = Requests()
             try:
                 el = table.query.filter_by(
                     email=response_json["email"]).one()
             except SQLAlchemyError:
                 return BARUtils.error_exit("Internal server error"), 500
             db.session.delete(el)
             db.session.commit()
             # table.query.filter_by(email=response_json['email']).delete()
             return BARUtils.success_exit(True)
         else:
             return BARUtils.error_exit("Forbidden"), 403
 def post(self):
     """This function adds a CSV's data to the database. This is only called by the Cromwell server after receiving the user's file."""
     if request.remote_addr != "127.0.0.1":
         return BARUtils.error_exit("Forbidden"), 403
     if request.method == "POST":
         key = request.headers.get("X-Api-Key")
         if SummarizationGeneExpressionUtils.decrement_uses(key):
             csv = request.get_json()["csv"]
             db_id = request.get_json()["uid"]
             df = pandas.read_csv(csv)
             db_id = db_id.split(".")[0]
             df = df.melt(id_vars=["Gene"],
                          var_name="Sample",
                          value_name="Value")
             db_id = db_id.split("/")[len(db_id.split("/")) - 1]
             con = db.get_engine(bind="summarization")
             df.to_sql(db_id, con, if_exists="append", index=True)
             return BARUtils.success_exit("Success")
         else:
             return BARUtils.error_exit("Invalid API key")