#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# TODO: Add qualifications for output items per specific context input item.
"""
mwtab.mwrest
~~~~~~~~~~~~

This module provides routines for accessing the Metabolomics Workbench REST API.

See https://www.metabolomicsworkbench.org/tools/MWRestAPIv1.0.pdf for details.
"""

from collections import OrderedDict
from . import fileio
import re
import json


VERBOSE = False
BASE_URL = "https://www.metabolomicsworkbench.org/rest/"


def analysis_ids(base_url=BASE_URL):
    """
    Method for retrieving a list of analysis ids for every current analysis in Metabolomics Workbench.

    :param str base_url: Base url to Metabolomics Workbench REST API.
    :return: List of every available Metabolomics Workbench analysis identifier.
    :rtype: :py:class:`list`
    """
    st_an_dict = _pull_study_analysis(base_url)
    analyses = list()
    [analyses.extend(st_an_dict[k]) for k in st_an_dict.keys()]

    if VERBOSE:
        print("Found {} Analysis Files to be Downloaded".format(len(analyses)))

    return analyses


def study_ids(base_url=BASE_URL):
    """
    Method for retrieving a list of study ids for every current study in Metabolomics Workbench.

    :param str base_url: Base url to Metabolomics Workbench REST API.
    :return: List of every available Metabolomics Workbench study identifier.
    :rtype: :py:class:`list`
    """
    st_an_dict = _pull_study_analysis(base_url)
    studies = list(st_an_dict.keys())

    if VERBOSE:
        print("Found {} Study Files to be Downloaded".format(len(studies)))

    return studies


def _pull_study_analysis(base_url):
    """
    Method for requesting a JSON string containing all study ids and analysis ids from Metabolomics Workbench's REST
    API. Requests a JSON file which contains a list of studies and their accompanying analyses. The JSON file is
    converted into a python object (dict) which can then be parsed to create a dictionary with the form study id (key):
    analysis id(s) (values).

    :param str base_url: Base url to Metabolomics Workbench REST API.
    :return: Dictionary of study ids (keys) and lists of analyses (value).
    :rtype: :py:class:`dict`
    """
    url = GenericMWURL(
        {"context": "study", "input_item": "study_id", "input_value": "ST", "output_item": "analysis"},
        base_url
    ).url
    mwrestfile = next(fileio.read_mwrest(url, **{"convertJSON": True}))
    json_object = mwrestfile._is_json(mwrestfile.text)

    study_analysis_dict = dict()
    for k in json_object.keys():
        if study_analysis_dict.get(json_object[k]["study_id"]):
            study_analysis_dict[json_object[k]["study_id"]].append(json_object[k]["analysis_id"])
        else:
            study_analysis_dict[json_object[k]["study_id"]] = [json_object[k]["analysis_id"]]

    return study_analysis_dict


def generate_mwtab_urls(input_items, base_url=BASE_URL, output_format='txt'):
    """
    Method for generating URLS to be used to retrieve `mwtab` files for analyses and
    studies through the REST API of the Metabolomics Workbench database.

    :param list input_items: List of Metabolomics Workbench input values for mwTab files.
    :param str base_url: Base url to Metabolomics Workbench REST API.
    :param str output_format: Output format for the mwTab files to be retrieved in.
    :return: Metabolomics Workbench REST URL string(s).
    :rtype: :py:class:`str`
    """
    for input_item in input_items:
        if input_item.isdigit():
            analysis_id = "AN{}".format(input_item.zfill(6))
            yield GenericMWURL({
                "context": "study",
                "input_item": "analysis_id",
                "input_value": analysis_id,
                "output_item": "mwtab",
                "output_format": output_format
            }, base_url).url
        elif re.match(r'(AN[0-9]{6}$)', input_item):
            yield GenericMWURL({
                "context": "study",
                "input_item": "analysis_id",
                "input_value": input_item,
                "output_item": "mwtab",
                "output_format": output_format
            }, base_url).url
        elif re.match(r'(ST[0-9]{1,6}$)', input_item):
            yield GenericMWURL({
                "context": "study",
                "input_item": "study_id",
                "input_value": input_item,
                "output_item": "mwtab",
                "output_format": output_format
            }, base_url).url


def generate_urls(input_items, base_url=BASE_URL, **kwds):
    """
    Method for creating a generator which yields validated Metabolomics Workbench REST urls.

    :param list input_items: List of Metabolomics Workbench input values for mwTab files.
    :param str base_url: Base url to Metabolomics Workbench REST API.
    :param dict kwds: Keyword arguments of Metabolomics Workbench URL Path items.
    :return: Metabolomics Workbench REST URL string(s).
    :rtype: :py:class:`str`
    """
    for input_item in input_items:
        params = dict(kwds)
        params["input_item"] = input_item
        yield GenericMWURL(params, base_url).url


class GenericMWURL(object):
    """GenericMWURL class that stores and validates parameters specifying a Metabolomics Workbench REST URL.

    Metabolomics REST API requests are performed using URL requests in the form of
        https://www.metabolomicsworkbench.org/rest/context/input_specification/output_specification

        where:
            if context = "study" | "compound" | "refmet" | "gene" | "protein"
                input_specification = input_item/input_value
                output_specification = output_item/[output_format]
            elif context = "moverz"
                input_specification = input_item/input_value1/input_value2/input_value3
                    input_item = "LIPIDS" | "MB" | "REFMET"
                    input_value1 = m/z_value
                    input_value2 = ion_type_value
                    input_value3 = m/z_tolerance_value
                output_specification = output_format
                    output_format = "txt"
            elif context =  "exactmass"
                input_specification = input_item/input_value1/input_value2
                    input_item = "LIPIDS" | "MB" | "REFMET"
                    input_value1 = LIPID_abbreviation
                    input_value2 = ion_type_value
                output_specification = None

    """
    context = {
        "study": {
            "input_item": {
                'study_id', 'study_title', 'institute', 'last_name', 'analysis_id', 'metabolite_id'
            },
            "output_item": {
                'summary', 'factors', 'analysis', 'metabolites', 'mwtab', 'source', 'species', 'disease',
                'number_of_metabolites', 'data', 'datatable', 'untarg_studies', 'untarg_factors', 'untarg_data'
            }
        },

        "compound": {
            "input_item": {
                'regno', 'formula', 'inchi_key', 'lm_id', 'pubchem_cid', 'hmdb_id', 'kegg_id', 'chebi_id', 'metacyc_id',
                'abbrev'
            },
            "output_item": {
                'all', 'regno', 'formula', 'exactmass', 'inchi_key', 'name', 'sys_name', 'smiles', 'lm_id',
                'pubchem_cid', 'hmdb_id', 'kegg_id', 'chebi_id', 'metacyc_id', 'classification', 'molfile', 'png'
            }
        },

        "refmet": {
            "input_item": {
                'all', 'match', 'name', 'inchi_key', 'regno', 'pubchem_cid', 'formula', 'main_class', 'sub_class'
            },
            "output_item": {
                'all', 'name', 'inchi_key', 'regno', 'pubchem_cid', 'exactmass', 'formula', 'synonyms', 'sys_name',
                'main_class', 'sub_class'
            }
        },

        "gene": {
            "input_item": {
                'mgp_id', 'gene_id', 'gene_name', 'gene_symbol', 'taxid'
            },
            "output_item": {
                'all', 'lmp_id', 'mgp_id', 'gene_name', 'gene_symbol', 'gene_synonyms', 'alt_names', 'chromosome',
                'map_location', 'summary', 'taxid', 'species', 'species_long'
            }
        },

        "protein": {
            "input_item": {
                'mgp_id', 'gene_id', 'gene_name', 'gene_symbol', 'taxid', 'mrna_id', 'refseq_id', 'protein_gi',
                'uniprot_id', 'protein_entry', 'protein_name'
            },
            "output_item": {
                'all', 'mgp_id', 'gene_id', 'gene_name', 'gene_symbol', 'taxid', 'species', 'species_long', 'mrna_id',
                'refseq_id', 'protein_gi', 'uniprot_id', 'protein_entry', 'protein_name', 'seqlength', 'seq',
                'is_identical_to'
            }
        },

        "moverz": {
            "input_item": {
                'LIPIDS', 'MB', 'REFMET'
            },
            "ion_type_value": {
                'M+H', 'M+H-H2O', 'M+2H', 'M+3H', 'M+4H', 'M+K', 'M+2K', 'M+Na', 'M+2Na', 'M+Li', 'M+2Li', 'M+NH4',
                'M+H+CH3CN', 'M+Na+CH3CN', 'M.NaFormate+H', 'M.NH4Formate+H', 'M.CH3', 'M.TMSi', 'M.tBuDMSi', 'M-H',
                'M-H-H2O', 'M+Na-2H', 'M+K-2H', 'M-2H', 'M-3H', 'M4H', 'M.Cl', 'M.F', 'M.HF2', 'M.OAc', 'M.Formate',
                'M.NaFormate-H', 'M.NH4Formate-H', 'Neutral'
            }
        },

        "exactmass": {
            "LIPID_abbreviation": {
                'ArthroCer', 'asialoGM2Cer', 'CAR', 'CE', 'Cer', 'CerP', 'CoA', 'DG', 'DGDG', 'FA', 'GalCer', 'GB3Cer',
                'GlcCer', 'GM3Cer', 'GM4Cer', 'iGB3Cer', 'LacCer', 'Lc3Cer', 'Manb1-4GlcCer', 'MG', 'MGDG', 'MolluCer',
                'PA', 'PC', 'PE', 'PE-Cer', 'PG', 'PGP', 'PI', 'PI-Cer', 'PIP', 'PIP2', 'PIP3', 'PS', 'SM', 'SQDG', 'TG'
            },
            "ion_type_value": {
                'Neutral', 'M+H', 'M+H-H2O', 'M+2H', 'M+3H', 'M+4H', 'M+K', 'M+2K', 'M+2K-H', 'M+Na', 'M+2Na',
                'M+2Na-H',
                'M+Li', 'M+2Li', 'M+Ag', 'M+NH4', 'M-H', 'M-CH3', 'M2H', 'M-3H', 'M-4H', 'M.Cl', 'M.OAc', 'M.Formate'
            }
        }
    }

    def __init__(self, rest_params, base_url=BASE_URL):
        """URL initializer.

        :param dict rest_params: Dictionary of Metabolomics Workbench URL Path items.
        :param str base_url: Base url to Metabolomics Workbench REST API.
        """
        self.rest_params = rest_params
        self.base_url = base_url
        self._validate()
        self.url = self._create_url()

    def _validate(self):
        """Validate URL parameters. Raises error if self.rest_params is lacking a "context" keyword. Sub-functions raise
        error if missing or invalid parameter(s) in self.rest_params.

        :return: None
        :rtype: :py:obj:`None`
        """
        if not self.rest_params["context"] in self.context.keys():
            raise KeyError("Error: Invalid/missing context")
        elif self.rest_params["context"] in {"study", "compound", "refmet", "gene", "protein"}:
            self._validate_generic()
        elif self.rest_params["context"] == "moverz":
            self._validate_moverz()
        elif self.rest_params["context"] == "exactmass":
            self._validate_exactmass()

    def _validate_generic(self):
        """Validates keyword arguments for study, compound, refmet, gene, and protein contexts. Raises error if missing
        or invalid parameter(s) in self.rest_params.

        context = "study"
        input_item = "study_id" | "study_title" | "institute" | "last_name" | "analysis_id" | "metabolite_id"
        input_value = input_value
        output_item = "summary" | "factors" | "analysis" | "metabolites" | "mwtab" | "source" | "species" | "disease" |
                      "number_of_metabolites" | "data" | "datatable" | "untarg_studies" | "untarg_factors" |
                      "untarg_data"
        output_format = "txt" | "json" [Default: "json"]

        context = "compound"
        input_item = "regno" | "formula" | "inchi_key" | "lm_id" | "pubchem_cid" | "hmdb_id" | "kegg_id" | "chebi_id" |
                      "metacyc_id" | "abbrev"
        input_value = input_value
        output_item = "all" | "regno" | "formula" | "exactmass" | "inchi_key" | "name" | "sys_name" | "smiles" |
                      "lm_id" | "pubchem_cid" | "hmdb_id" | "kegg_id" | "chebi_id" | "metacyc_id" | "classification" |
                      "molfile" | "png" | "regno,formula,exactmass,..."
        output_format = "txt" | "json" [Default: "json"]

        context = "refmet"
        input_item = "all" | "match" | "name" | "inchi_key" | "regno" | "pubchem_cid" | "formula" | "main_class" |
                     "sub_class"
        input_value = input_value
        output_item = "all" | "name" | "inchi_key" | "regno" | "pubchem_cid" | "exactmass" | "formula" | "synonyms" |
                       "sys_name" | "main_class" | "sub_class" | "name,inchi_key,regno,..."
        output_format = "txt" | "json" [Default: "json"]

        context = "gene"
        input_item = "mgp_id" | "gene_id" | "gene_name" | "gene_symbol" | "taxid"
        input_value = input_value
        output_item = "all" | "lmp_id" | "mgp_id" | "gene_name" | "gene_symbol" | "gene_synonyms" | "alt_names" |
                      "chromosome" | "map_location" | "summary" | "taxid" | "species" | "species_long" |
                      "mgp_id,gene_id,gene_name,..."
        output_format = "txt" | "json" [Default: "json"]

        context = "protein"
        input_item = "mgp_id" | "gene_id" | "gene_name" | "gene_symbol" | "taxid" | "mrna_id" | "refseq_id" |
                     "protein_gi" | "uniprot_id" | "protein_entry" | "protein_name"
        input_value = input_value
        output_item = "all" | "mgp_id" | "gene_id" | "gene_name" | "gene_symbol" | "taxid" | "species" |
                      "species_long" | "mrna_id" | "refseq_id" | "protein_gi" | "uniprot_id" | "protein_entry" |
                      "protein_name" | "seqlength" | "seq" | "is_identical_to" | "mgp_id,gene_id,gene_name,..."
        output_format = "txt" | "json" [Default: "json"]

        Uses static method self._validate_id() to validate input_value parameter.

        :return: None
        :rtype: :py:obj:`None`
        """
        keywords = {"input_item", "input_value", "output_item"}
        # validate all required parameters are present
        if not all(k in self.rest_params.keys() for k in keywords):
            raise KeyError("Missing input item(s): " + str(keywords.difference(self.rest_params.keys())))

        # validate input_item
        if not any(k in self.rest_params["input_item"] for k in self.context[self.rest_params["context"]]["input_item"]):
            raise ValueError("Invalid input item")

        # validate input_item
        self._validate_input(self.rest_params["input_item"], self.rest_params["input_value"])

        # validate output_item(s)
        if type(self.rest_params["output_item"]) == list:
            if self.rest_params["context"] == "study":
                raise ValueError("Invalid output items. Study only takes a single output item.")
            elif not all(k in self.context[self.rest_params["context"]]["output_item"] for k in self.rest_params["output_item"]):
                raise ValueError("Invalid output item(s): " +
                                 str(set(self.rest_params["output_item"]).difference(self.context[self.rest_params["context"]]["output_item"])))
        elif not any(k in self.rest_params["output_item"] for k in self.context[self.rest_params["context"]]["output_item"]):
            raise ValueError("Invalid output item")

    def _validate_moverz(self):
        """Validate keyword arguments for moverz context. Raises error if missing or invalid parameter(s) in
        self.rest_params.

        context = "moverz"
        input_item = "LIPIDS" | "MB" | "REFMET"
        input_value1 = m/z_value
        input_value2 = ion_type_value
        input_value3 = m/z_tolerance_value
        output_format = "txt"

        m/z_value range: 50-2000
        See CONTEXT variable for supported ion type values.
        m/z_tolerance_value range: 0.0001-1

        :return: None
        :rtype: :py:obj:`None`
        """
        keywords = {"input_item", "m/z_value", "ion_type_value", "m/z_tolerance_value"}
        if not all(k in self.rest_params.keys() for k in keywords):
            raise KeyError("Missing input item(s): " + str(keywords.difference(self.rest_params.keys())))
        elif not any(k in self.rest_params["input_item"] for k in self.context["moverz"]["input_item"]):
            raise ValueError("Invalid input item")
        elif not 50 <= float(self.rest_params["m/z_value"]) <= 2000:
            raise ValueError("m/z value outside of range: 50-2000")
        elif not self.rest_params["ion_type_value"] in self.context["moverz"]["ion_type_value"]:
            raise ValueError("Invalid ion type value")
        elif not 0.0001 <= float(self.rest_params["m/z_tolerance_value"]) <= 1:
            raise ValueError("m/z tolerance value outside of range: 0.0001-1")

    def _validate_exactmass(self):
        """Validate keyword arguments for exactmass context. Raises error if missing or invalid parameter(s) in
        self.rest_params.

        context = "exactmass"
        LIPID_abbreviation = ...
        ion_type_value = ...

        See :class:`~mwtab.mwrest.GenericMWURL.context` variable for full list of possible values for LIPID_abbreviation
        and ion_type_value.

        :return: None
        :rtype: :py:obj:`None`
        """
        keywords = {"LIPID_abbreviation", "ion_type_value"}
        if not all(k in self.rest_params.keys() for k in keywords):
            raise KeyError("Missing input item(s): " + str(keywords.difference(self.rest_params.keys())))
        elif not any(k in self.rest_params["LIPID_abbreviation"] for k in self.context["exactmass"]["LIPID_abbreviation"]):
            raise ValueError("Invalid LIPID abbreviation")
        elif not self.rest_params['ion_type_value'] in self.context["exactmass"]["ion_type_value"]:
            raise ValueError("Invalid ion type value")

    @staticmethod
    def _validate_input(input_item, input_value):
        """Validate keyword arguments for input item where an id is used (ie. study, compound, refmet, gene, and
        protein). If invalid, raises value error.

        :param str input_item: String representing the input item from the input specification.
        :param str input_value: String representing the input vlaue from the input specification.
        :return: None
        :rtype: :py:obj:`None`
        """
        if input_item == "study_id":
            # allows for pulling a range of entries (ie. ST0001 pulls studies 100-199)
            if not re.match(r"(ST[0-9]{0,6}$)", input_value):
                raise ValueError("Invalid Metabolomics Workbench (MW) study ID (ST<6-digit integer>)")
        elif input_item in ["study_title", "institute", "last_name",
                            "formula",
                            "gene_name", "gene_symbol", "protein_entry", "protein_name"]:
            if not type(input_value) == str:
                raise ValueError("Invalid {} (<string>)".format(input_item.replace("_", " ")))
        elif input_item == "analysis_id":
            if not re.match(r'(AN[0-9]{6}$)', input_value):
                raise ValueError("Invalid Metabolomics Workbench analysis ID for a study (AN<6-digit integer>)")
        elif input_item == "metabolite_id":
            if not re.match(r"(ME[0-9]{6}$)", input_value):
                raise ValueError("Invalid Metabolomics Workbench metabolite ID for a study (ME<6-digit integer>)")

        elif input_item == "regno":
            if not input_value.isdigit():
                raise ValueError("Invalid Metabolomics Workbench Metabolite database ID (<integer>)")
        elif input_item == "inchi_key":
            if not re.match(r"([A-Z,\-]{27}$)", input_value):
                raise ValueError("Invalid InChIKey (27-character string)")
        elif input_item == "lm_id":
            if not re.match(r"(LM[A-Z]{2}[0-9]{8,10}$)", input_value):
                raise ValueError("Invalid LIPID MAPS ID (LM<2-character LIPID MAPS category><8-10 character string>)")
        elif input_item == "pubchem_cid":
            if not input_value.isdigit():
                raise ValueError("Invalid PubChem Compound ID (<integer>)")
        elif input_item == "hmdb_id":
            if not re.match(r"(HMDB[0-9]+$)", input_value):
                raise ValueError("Invalid Human Metabolome Database ID (HMDB<integer>)")
        elif input_item == "kegg_id":
            if not re.match(r"(CO[0-9]+$)", input_value):
                raise ValueError("Invalid KEGG compound ID (CO<integer>)")
        elif input_item == "chebi_id":
            if not input_value.isdigit():
                raise ValueError("Invalid ChEBI compound id (<integer>)")
        # TODO: update the following two input types.
        elif input_item == "metacyc_id":
            if not type(input_value) == str:
                raise ValueError("Invalid METACYC compound ID (<string>)")
        elif input_item == "abbrev":
            if not type(input_value) == str:
                raise ValueError("Invalid : Lipid bulk abbreviation (<string>)")

        elif input_item == "mgp_id":
            if not re.match(r"(MGP[0-9]{6}$)", input_value):
                raise ValueError("Invalid Human Metabolome Gene/Protein (MGP) database gene ID (MGP<6-digit integer>)")
        elif input_item == "gene_id":
            if not input_value.isdigit():
                raise ValueError("Invalid Entrez gene ID (<integer>)")
        elif input_item == "taxid":
            if not input_value.isdigit():
                raise ValueError("Invalid NCBI taxonomy ID (<integer>)")
        elif input_item == "mrna_id":
            if not re.match(r"(NM_[0-9]+$)", input_value):
                raise ValueError("Invalid mRNA ID (NM_<integer>)")
        elif input_item == "refseq_id":
            if not re.match(r"(NP_[0-9]+$)", input_value):
                raise ValueError("Invalid mRNA ID (NP_<integer>)")
        elif input_item == "protein_gi":
            if not input_value.isdigit():
                raise ValueError("Invalid NCBI protein GI (<integer>)")
        elif input_item == "uniprot_id":
            # regex from https://www.uniprot.org/help/accession_numbers
            if not re.match(r"[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9]([A-Z][A-Z0-9]{2}[0-9]){1,2}", input_value):
                raise ValueError("Invalid UniProt ID (see uniprot.org/help/accession_numbers)")

    def _create_url(self):
        """Method for constructing a formatted Metabolomics Workbench REST API URL from the given class parameters.

        :return: Formatted Metabolomics Workbench REST API URL.
        :rtype: :py:class:`str`
        """
        if self.rest_params["context"] in {"study", "compound", "refmet", "gene", "protein"}:
            # allows for url to include or not include output_format
            if self.rest_params.get("output_format"):
                return self.base_url + "/".join([self.rest_params.get(p) for p in [
                    "context", "input_item", "input_value", "output_item", "output_format"
                ]])
            else:
                return self.base_url + "/".join([self.rest_params.get(p) for p in [
                    "context", "input_item", "input_value", "output_item"
                ]])

        elif self.rest_params["context"] == "moverz":
            rest_params = [self.rest_params.get(p) for p in [
                "context", "input_item", "m/z_value", "ion_type_value", "m/z_tolerance_value"
            ]]
            rest_params.append("txt")
            return self.base_url + "/".join(rest_params)

        elif self.rest_params["context"] == "exactmass":
            return self.base_url + "/".join([self.rest_params.get(p) for p in [
                "context", "LIPID_abbreviation", "ion_type_value"
            ]])


class MWRESTFile(object):
    """MWRESTFile class that stores data from a single file download through Metabolomics Workbench's REST API.

    Mirrors :class:`~mwtab.mwtab.MWTabFile`.
    """

    def __init__(self, source):
        """File initializer.

        :param str source: Source a `MWRESTFile` instance was created from.
        """
        self.source = source
        self.text = ""

    def read(self, filehandle):
        """Read data into a :class:`~mwtab.mwrest.MWRESTFile` instance.

        :param filehandle: file-like object.
        :type filehandle: :py:class:`io.TextIOWrapper`, :py:class:`gzip.GzipFile`,
                          :py:class:`bz2.BZ2File`, :py:class:`zipfile.ZipFile`
        :return: None
        :rtype: :py:obj:`None`
        """
        self.text = filehandle.read().decode("utf-8")
        # input_str = input_str.replace("\r\n", "\n")
        # self.text = re.sub(r"</br>", "", self.text)  # included to remove remaining HTML tags
        filehandle.close()

    def write(self, filehandle):
        """Write :class:`~mwtab.mwrest.MWRESTFile` data into file.

        :param filehandle: file-like object.
        :type filehandle: :py:class:`io.TextIOWrapper`
        :return: None
        :rtype: :py:obj:`None`
        """
        try:
            filehandle.write(self.text)
        except IOError:
            raise IOError('"filehandle" parameter must be writable.')
        filehandle.close()

    @staticmethod
    def _is_json(string):
        """Test if input string is in JSON format.

        :param string: Input string.
        :type string: :py:class:`str` or :py:class:`bytes`
        :return: Input string if in JSON format or False otherwise.
        :rtype: :py:class:`str` or :py:obj:`False`
        """
        try:
            if isinstance(string, bytes):
                json_str = json.loads(string.decode("utf-8"), object_pairs_hook=OrderedDict)
            elif isinstance(string, str):
                json_str = json.loads(string, object_pairs_hook=OrderedDict)
            else:
                raise TypeError("Expecting <class 'str'> or <class 'bytes'>, but {} was passed".format(type(string)))
            return json_str
        except ValueError:
            return False
