from __future__ import unicode_literals
import json
import warnings

from .base import BaseModel, ensure_string_list
from .enums import (AssayColumnType, AttachmentType, ColorStyleType, LigandDesignerOverlayType,
                    DockingPrecision, PoseFormat, PropertyAclLevel, PropertyCategory, PropertyManagedBy,
                    PropertySubCategory, StructureTransformation)

# NOTE(badlato): The following are imported here for backwards compatibility (moved in 8.10)
from .requests import (  # noqa: F401
    ChangedEntity, ChangedLiveReport, ChangedObservation, RemovedLiveReportRow)
from .base import ModelEncoder  # noqa: F401
from .exceptions import (  # noqa: F401
    SchrodingerException, InvalidArgumentException, AsyncServiceTaskFailedError)


class Project(BaseModel):
    """
    The Project class, which holds the base information for each project.

    :type id: :class:`str`
    :param id: The unique numeric id assigned to the project

    :type name: :class:`str`
    :param name: The given name for the project

    :type active: :class:`str`
    :param active: The activity status for the project:

        - `Y`
        - `N`

    :type alternate_id: :class:`str`
    :param alternate_id: Field is unused in LiveDesign, it is only kept for backwards compatibility.

    :type default_template_id: :class:`str`
    :param default_template_id: The LiveReport id of the default template for the Project. This is default to 0.

    :type description: :class:`str`
    :param description: The description for the given project.

    :type restricted: :class:`bool`
    :param restricted: Whether or not the project is restricted.

    :type keep_styles: :class:`bool`
    :param keep_styles: Whether or not to keep the styles are kept

    :type therapeutic_area: :class:`str`
    :param therapeutic_area: The therapeutic area specific to the project

    :type project_lead: :class:`str`
    :param project_lead: The person or group responsible for the project.
    """
    _fields = [
        'id', 'name', 'active', 'alternate_id', 'default_template_id', 'description', 'restricted',
        'keep_styles', 'therapeutic_area', 'project_lead'
    ]

    def __init__(self,
                 id=None,
                 name=None,
                 active='Y',
                 alternate_id=None,
                 default_template_id=0,
                 description=None,
                 restricted=False,
                 keep_styles=False,
                 therapeutic_area=None,
                 project_lead=None):
        self.id = id
        self.name = name
        self.active = active
        self.alternate_id = alternate_id if alternate_id is not None else name
        self.default_template_id = default_template_id
        self.description = description
        self.restricted = restricted
        self.keep_styles = keep_styles
        self.therapeutic_area = therapeutic_area
        self.project_lead = project_lead


class AssayType(BaseModel):
    """
    The information per assay type, which is each column in a report.

    :type name: :class:`str`
    :param name: The name of the column.

    :type count: :class:`int`
    :param count: The count of items in the assay.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column id

    :type value_type: :class:`str`
    :param value_type: What type of data can be stored in the column. Options are:

        -"integer"
        -"float"
        -"string"
        -"pdb_data"
        -"rationale"
        -"molecule"
        -"date"
        -"datetime"
        -"attachment"
        -"3D"
        -"variable" (Meant as a palceholder for Formula and New Assays)
        -"image"
        -"boolean"

    :type column_type: :class:`~ldclient.enums.AddableColumnType`
    :param column_type: The type of the column.

    :type project_ids: :class:`str`
    :param project_ids: Which project ids in which the column exists.
    """

    _fields = ['name', 'count', 'addable_column_id', 'value_type', 'column_type', 'project_ids']

    def __init__(self,
                 name,
                 count=None,
                 addable_column_id=None,
                 value_type=None,
                 column_type=None,
                 project_ids=None):
        self.name = name
        self.count = count
        self.addable_column_id = addable_column_id
        self.value_type = value_type
        self.column_type = column_type
        self.project_ids = project_ids


class Assay(BaseModel):
    """
    An assay contains at least one type, and may contain a url.

    :type protocol: :class:`str`
    :param protocol: Protocol name for the assay.

    :type name: :class:`str`
    :param name: Name for the assay.

    :type database_name: :class:`str`
    :param database_name: The database name for the assay.

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: The IDs for the projects that use the assay.

    :type units: :class:`str`
    :param units: The assay unit name.

    :type lot: :class:`str`
    :param lot: The assay lot.

    :type concentration_units: :class:`str`
    :param concentration_units: The assay concentration unit name.

    :type concentration: :class:`str`
    :param concentration: The concentration of the assay.

    :type types: :class:`list` of :class:`AssayType`
    :param types: A list of the assay types.

    :type folder_path: :class:`str`
    :param folder_path: The assay folder path.
    """

    _fields = [
        'protocol', 'name', 'database_name', 'project_ids', 'units', 'lot', 'concentration_units',
        'concentration', 'types', 'folder_path'
    ]

    def __init__(self,
                 name,
                 types,
                 protocol=None,
                 database_name=None,
                 project_ids=None,
                 units=None,
                 lot=None,
                 concentration_units=None,
                 concentration=None,
                 folder_path=None):
        self.protocol = protocol
        self.name = name
        self.database_name = database_name
        self.project_ids = project_ids
        self.units = units
        self.lot = lot
        self.concentration_units = concentration_units
        self.concentration = concentration
        self.types = types
        self.folder_path = folder_path

    def serialize_types(self):
        """
        Retrieves a list all of the assay types in dictionary form for the current instance of Assay.

        :rtype: :class:`dict`
        :return: A list of all of the assay types.
        """
        return [array_type.as_dict() for array_type in self.types]

    @staticmethod
    def deserialize_types(types):
        """
        Deserializes a given list of types.

        :type types: :class:`list` of :class:`dict`
        :param types: A list of dictionaries describing the assay types.

        :rtype: :class:`list` of :class:`AssayType`
        :return: The list of AssayTypes being described.
        """
        return AssayType.from_list(types)


class DataSourceAssayFolderRules(BaseModel):
    """
    A DataSourceAssayFolderRules object contains a data source id and a list of individual AssayFolderRule, the last
    rule in that list must be a default catch-all rule

    :type id: :class:`str`
    :param id: Each DataSourceAssayFolderRules object has a unique numeric id assigned to it.

    :type data_source_id: :class:`str`
    :param data_source_id: Specifies the id of the data source for which the list of rules are applicable

    :type rules: :class:`AssayFolderRule`
    :param rules: The rules used to determine the folder which an assay will be placed in the column tree.
    """

    _fields = ['id', 'data_source_id', 'rules']

    def __init__(self, id=None, data_source_id=None, rules=None):
        self.id = id
        self.data_source_id = data_source_id
        self.rules = rules


class AssayFolderRule(BaseModel):
    """
    An assay folder rule is used to determine the folder into which an Assay would be placed in the column tree. The
    folder is determined by going through a list of such rules, the FIRST rule(the priority of the rule is determined by
    its position in the list with a higher priority for a rule which occurs before another rule in the list) that
    matches for an Assay is used for picking the folder. The last rule in those set of rules is a default catch-all rule
    which matches all the Assays.

    :type id: :class:`str`
    :param id: Each assay folder rule has a unique numeric id assigned to it.

    :type match_type: :class:`str`
    :param match_type: String with supported values ['STRING', 'DATE']
            Each value specifies type of the assay folder rule
            (*Must be STRING for the default catch-all rule*)

    :type comparison_type: :class:`str`
    :param comparison_type: String with supported values ['EQUALS', 'EQUALS_IGNORE_CASE', 'STARTS_WITH', 'ENDS_WITH',
            'INDEX_OF', 'LAST_INDEX_OF', 'REGEX', 'STARTS_WITH_LIST', 'ENDS_WITH_LIST']
            Each value specifies the match operator used for comparison to determine if an Assay satisfies the given
            Assay folder rule.
            (*Must be STARTS_WITH for the default catch-all rule*)

    :type match_operand: :class:`str`
    :param match_operand: Specifies the match criteria used for a specific Assay folder rule, you can use a pipe
            character '|' to define several match criteria. eg - 'IV | PO'
            (*Must be "" for the default catch-all rule*)

    :type folder: :class:`str`
    :param folder: Specifies the folder where the assay would be placed if it matches the given Assay folder rule. Use a
            forward slash '/' to define nested folders. eg - '/Assay/PK/Mouse'
    """

    _fields = ['id', 'match_type', 'comparison_type', 'match_operand', 'folder']

    def __init__(self,
                 id=None,
                 match_type=None,
                 comparison_type=None,
                 match_operand=None,
                 folder=None):
        self.id = id
        self.match_type = match_type
        self.comparison_type = comparison_type
        self.match_operand = match_operand
        self.folder = folder


class AssayDefaultAggregationRule(BaseModel):
    """
    An assay default aggregation rule is used to determine the 'default' aggregation mode of an Assay till it is
    explicitly overridden by the user. The default aggregation mode of an Assay is determined by going through a list of
    such rules, the FIRST rule(the priority of the rule is determined by its position in the list with a higher priority
    for a rule which occurs before another rule in the list) that matches for an Assay is used for picking the default
    aggregation mode. In case none of the rules match, there is a server wide rule(the last rule in the list) which
    would be used which matches all the Assays.

    :type id: :class:`str`
    :param id: Each assay default aggregation rule has a unique numeric id assigned to it.

    :type match_target: :class:`str`
    :param match_target: String with supported values ['ASSAY_RESULT_TYPE', 'ASSAY_RESULT_NAME']
            Each value specifies whether to match an assay using its name or type
            (*Must be None for the server wide rule*)

    :type match_operator: :class:`str`
    :param match_operator: String with supported values ['EQUALS', 'EQUALS_IGNORE_CASE', 'STARTS_WITH', 'ENDS_WITH',
            'INDEX_OF', 'LAST_INDEX_OF', 'REGEX', 'STARTS_WITH_LIST', 'ENDS_WITH_LIST']
            Each value specifies the match operator used for comparison to determine if an Assay satisfies the given
            Assay default aggregation rule.
            (*Must be None for the server wide rule*)

    :type match_operand: :class:`str`
    :param match_operand: Specifies the match criteria used for a specific Assay default aggregation rule, you can use a pipe
            character '|' to define several match criteria. eg - 'IV | PO'
            (*Must be None for the server wide rule*)

    :type method: :class:`str`
    :param method: String with supported values ['median', 'arithmetic_mean', 'geometric_mean', 'logit_mean', 'harmonic_mean',
            'std_dev', 'count', 'min', 'max', 'latest', 'unaggregated']
            Each value specifies the cell aggregation type to be used for an assay matching the given rule

    :type exclude_operators: :class:`bool`
    :param exclude_operators: Boolean value which sets whether the operators will be excluded from the calculation of the
            assay result.

    EXAMPLE:

         {"ASSAY_RESULT_TYPE", "EQUALS", "Ki", "geometric_mean", "true"}

        Results in all Assays with a result type of "Ki" displaying an assay result that is the geometric mean of
        the values without operators.

         {"ASSAY_RESULT_TYPE", "EQUALS", "EC50", "arithmetic_mean", "false"}

        Results in all Assays with a result type of "EC50" displaying an assay result that is the arithmetic mean of
        the values with operators.
    """

    _fields = [
        'id', 'match_target', 'match_operator', 'match_operand', 'method', 'exclude_operators'
    ]

    def __init__(self,
                 id=None,
                 match_target=None,
                 match_operator=None,
                 match_operand=None,
                 method=None,
                 exclude_operators=None):
        self.id = id
        self.match_target = match_target
        self.match_operator = match_operator
        self.match_operand = match_operand
        self.method = method
        self.exclude_operators = exclude_operators


class Column(BaseModel):
    """
    The Column Class Information

    :type id: :class:`str`
    :param id: The unique column id.

    :type name: :class:`str`
    :param name: The name for the column.

    :type column_type: :class:`str`
    :param column_type: The type of column.

    :type value_type: :class:`str`
    :param value_type: The type of data that can be stored in the column. Options are:

        -"integer"
        -"float"
        -"string"
        -"pdb_data"
        -"rationale"
        -"molecule"
        -"date"
        -"datetime"
        -"attachment"
        -"3D"
        -"variable" (Meant as a palceholder for Formula and New Assays)
        -"image"
        -"boolean"

    :type log_scale: :class:`bool`
    :param log_scale: A boolean of whether or not a logarithmic scale should be used.

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: A list of projects that the column belongs.

    :type folder_name: :class:`str`
    :param folder_name: The folder name that the column belongs. Options are:

        -"computational_models"
        -"experimental_assays"
    """
    _fields = ['id', 'name', 'column_type', 'value_type', 'log_scale', 'project_ids', 'folder_name']

    def __init__(self,
                 id=None,
                 name=None,
                 column_type=None,
                 value_type=None,
                 log_scale=None,
                 project_ids=None,
                 folder_name=None):
        self.id = id
        self.name = name
        self.column_type = column_type
        self.value_type = value_type
        self.log_scale = log_scale
        self.project_ids = project_ids
        self.folder_name = folder_name


class ColumnarLigandDesignerConfiguration(BaseModel):
    """
    Class representing Columnar Ligand Designer Configuration which stores data required to perform
    ligand designer tasks.

    Columnar Ligand Designer Configuration can be explicit or implicit.

    An explicit configuration is associated with an addable column of type LIGAND_DESIGNER.

    An implicit configuration is associated with a 3d model return column.

    :type id: :class:`str`
    :param id: Unique ID of the columnar ligand designer configuration.

    :type name: :class:`str`
    :param name: Name of the columnar ligand designer configuration.

    :type description: :class:`str`
    :param description: Description of the columnar ligand designer configuration.

    :type folder: :class:`str`
    :param folder: Location of the columnar ligand designer configuration.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier for the addable column id.

    :type grid_file_attachment_id: :class:`str`
    :param grid_file_attachment_id: Alternate attachment id of grid file for the columnar ligand designer configuration.

    :type protein_attachment_id: :class:`str`
    :param protein_attachment_id: Alternate attachment id of protein file for the columnar ligand designer configuration.

    :type reference_ligand_attachment_ids: :class:`list` of :class:`str`
    :param reference_ligand_attachment_ids: List of alternate attachment ids of reference ligands for the columnar ligand designer configuration.

    :type overlays: :class:`list` of :class:`LigandDesignerOverlay`
    :param overlays: List of ligand designer overlays for the columnar ligand designer configuration.

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: A list of what projects use the columnar ligand designer configuration.

    :type docking_precision: :class:`~ldclient.enums.DockingPrecision`
    :param docking_precision: Precision to use for Glide Docking.

    :type user: :class:`str`
    :param user: Username of creator of the columnar ligander designer configuration.

    :type created_at: :class:`int`
    :param created_at: The timestamp that the columnar ligander designer configuration was created at.

    :type updated_at: :class:`int`
    :param updated_at: The timestamp that the columnar ligander designer configuration was updated at.
    """
    _fields = [
        'id', 'name', 'description', 'folder', 'addable_column_id', 'grid_file_attachment_id',
        'protein_attachment_id', 'reference_ligand_attachment_ids', 'overlays', 'project_ids',
        'docking_precision', 'user', 'created_at', 'updated_at'
    ]

    def __init__(self,
                 id=None,
                 name=None,
                 description=None,
                 addable_column_id=None,
                 folder=None,
                 grid_file_attachment_id=None,
                 protein_attachment_id=None,
                 reference_ligand_attachment_ids=None,
                 overlays=None,
                 project_ids=None,
                 docking_precision=None,
                 user=None,
                 created_at=None,
                 updated_at=None):
        self.id = id
        self.name = name
        self.description = description
        self.folder = folder
        self.addable_column_id = addable_column_id
        self.grid_file_attachment_id = grid_file_attachment_id
        self.protein_attachment_id = protein_attachment_id
        self.reference_ligand_attachment_ids = reference_ligand_attachment_ids
        self.overlays = overlays
        self.project_ids = project_ids
        self.docking_precision = docking_precision
        self.user = user
        self.created_at = created_at
        self.updated_at = updated_at

    def serialize_overlays(self):
        """
        Serialize the ligand designer overlays as a dictionary

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of ligand designer overlays, in dictionary form
        """
        if not self.overlays:
            return []
        return [overlay.as_dict() for overlay in self.overlays]

    @staticmethod
    def deserialize_overlays(overlays):
        """
        Deserializes the given ligand designer overlays from dictionary form to a list of :class:`LigandDesignerOverlay`.

        :type overlays: :class:`list` of :class:`dict`
        :param overlays: A list of dictionaries of ligand designer overlays

        :rtype: :class:`list` of :class:`LigandDesignerOverlay`
        :return: A list of ligand designer overlays.
        """
        if not overlays:
            return []
        return [LigandDesignerOverlay.from_dict(overlay) for overlay in overlays]
    
    @staticmethod
    def deserialize_docking_precision(docking_precision):
        return DockingPrecision(docking_precision.upper())


class ColumnGroup(BaseModel):
    """
    Class representing grouped columns in a LiveReport, and the order in which they appear.
    NOTE: An "ungrouped column" is represented by a single-member column group. In other words, a :class:`ColumnGroup` object with
    columns_order of length 1.

    :type id: :class:`str`
    :param id: ID of the column group

    :type name: :class:`str`
    :param name: Name of the column group

    :type frozen: :class:`bool`
    :param frozen: True if the column is frozen, False otherwise

    :type columns_order: :class:`list` of :class:`str`
    :param columns_order: Ordered list of the IDs of the columns in the group

    :type limiting_condition: :class:`LimitingCondition` or :obj:`None`
    :param limiting_condition: The limiting condition for the column group, if it is a limited assay group
    """

    _fields = ['id', 'name', 'frozen', 'columns_order', 'limiting_condition']

    def __init__(self,
                 id=None,
                 name=None,
                 frozen=None,
                 columns_order=None,
                 limiting_condition=None):
        self.id = id
        self.name = name
        self.frozen = frozen
        self.columns_order = columns_order
        self.limiting_condition = LimitingCondition.from_dict(limiting_condition) if isinstance(
            limiting_condition, dict) else limiting_condition

    def as_dict(self):
        """
        Returns the ColumnGroup as a dictionary with the limiting_condition as a dictionary as well, if it exists.

        :rtype: :class:`dict`
        :return: The current fields of the ColumnGroup as a dictionary.
        """
        result = super(ColumnGroup, self).as_dict()
        if result['limiting_condition']:
            result['limiting_condition'] = result['limiting_condition'].as_dict()
        return result


class DecimalFormattingRule(BaseModel):
    """
    A decimal formatting rule is used to control how decimal assay results of particular magnitude are to be formatted
    for display and how decimal results are displayed in excel exports(df0 through df11). When each formatting rule will
    be applied is described below-

    If d represents the decimal value to be formatted then

    When d > 0.0

       - d < 0.000001 df0 is used
       - d < 0.00001 df1 is used
       - d < 0.0001 df2 is used
       - d < 0.001 df3 is used
       - d < 0.01 df4 is used
       - d < 0.1 df5 is used
       - d < 1.0 df6 is used
       - d < 10.0 df7 is used
       - d < 100.0 df8 is used
       - d < 1000.0 df9 is used
       - d < 10000.0 df10 is used
       - otherwise df11 is used

    when d < 0.0

       - d > -0.000001 df0 is used
       - d > -0.00001 df1 is used
       - d > -0.0001 df2 is used
       - d > -0.001 df3 is used
       - d > -0.01 df4 is used
       - d > -0.1 df5 is used
       - d > -1.0 df6 is used
       - d > -10.0 df7 is used
       - d > -100.0 df8 is used
       - d > -1000.0 df9 is used
       - d > -10000.0 df10 is used
       - otherwise df11 is used

    when d is zero df7 is used

    The default values that would be used if none
    of the properties were set are given below

    For more information on the syntax for the decimal format specifications see

    https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html

    :type name: :class:`str`
    :param name: String name of the decimal formatting rule with supported values - ['df0', 'df1', 'df2', 'df3', 'df4',
        'df5', 'df6', 'df7', 'df8', 'df9', 'df10', 'df11', ]

    :type value: :class:`str`
    :param value: Decimal format specification to be used

    :type default_value: :class:`str`
    :param default_value: Default value that is used when no value is provided

    EXAMPLE:

         {"df7", "0.00##", "0.0#"}

        Results in all Assays with a value of 9 would be formatted to 9.00
    """

    _fields = ['name', 'value', 'default_value']

    def __init__(self, name=None, value=None, default_value=None):
        self.name = name
        self.value = value
        self.default_value = default_value


class LigandDesignerOverlay(BaseModel):
    """
    :type id: :class:`str`
    :param id: ID of Ligand Designer Overlay

    :type type: :class:`~ldclient.enums.LigandDesignerOverlayType`
    :param type: Type of Ligand Designer Overlay

    :type attachment_id: :class:`str`
    :param attachment_id: Alternate id of attachment
    """
    _fields = ['id', 'type', 'attachment_id']

    def __init__(self, id=None, type=None, attachment_id=None):
        self.id = id
        self.type = type
        self.attachment_id = attachment_id

    @staticmethod
    def deserialize_type(type):
        return LigandDesignerOverlayType.from_str(type)


class LimitingCondition(BaseModel):
    """
    Class representing a limiting condition of a limited assay column group.

    :type id: :class:`str`
    :param id: ID of the limiting condition

    :type assay_id: :class:`str`
    :param assay_id: ID of the assay

    :type date_inverted: :class:`bool`
    :param date_inverted: True if the date is inverted, false otherwise

    :type date_value_type: :class:`~ldclient.enums.QueryConditionValueType`
    :param date_value_type: Date value type.

    :type date_value: :class:`str`
    :param date_value: Date value

    :type inverted: :class:`bool`
    :param inverted: True if the value is inverted, False otherwise

    :type value_type: :class:`~ldclient.enums.QueryConditionValueType`
    :param value_type: Value type.

    :type value: :class:`str`
    :param value: Value

    :type delimiter: :class:`str`
    :param delimiter: Delimiter

    :type protocols: :class:`str`
    :param protocols: Protocols

    :type result_units: :class:`str`
    :param result_units: Result units

    :type concentrations: :class:`str`
    :param concentrations: Concentrations

    :type concentration_units: :class:`str`
    :param concentration_units: Concentration units

    :type related_column_conditions: :class:`list` of :class:`RelatedColumnCondition`
    :param related_column_conditions: Related column conditions
    """

    _fields = [
        'id', 'assay_id', 'date_inverted', 'date_value_type', 'date_value', 'inverted',
        'value_type', 'value', 'delimiter', 'protocols', 'result_units', 'concentrations',
        'concentration_units', 'related_column_conditions'
    ]

    def __init__(self,
                 id=None,
                 assay_id=None,
                 date_inverted=None,
                 date_value_type=None,
                 date_value=None,
                 inverted=None,
                 value_type=None,
                 value=None,
                 delimiter=None,
                 protocols=None,
                 result_units=None,
                 concentrations=None,
                 concentration_units=None,
                 related_column_conditions=[]):
        self.id = id
        self.assay_id = assay_id
        self.date_inverted = date_inverted
        self.date_value_type = date_value_type
        self.date_value = date_value
        self.inverted = inverted
        self.value_type = value_type
        self.value = value
        self.delimiter = delimiter
        self.protocols = protocols
        self.result_units = result_units
        self.concentrations = concentrations
        self.concentration_units = concentration_units
        self.related_column_conditions = [
            RelatedColumnCondition.from_dict(related_column_condition) if isinstance(
                related_column_condition, dict) else related_column_condition
            for related_column_condition in related_column_conditions
        ]

    def as_dict(self):
        """
        Returns the LimitingCondition as a dictionary with the related_column_conditions as a dictionary as well.

        :rtype: :class:`dict`
        :return: The current fields of the LimitingCondition as a dictionary.
        """
        result = super(LimitingCondition, self).as_dict()
        result['related_column_conditions'] = [
            related_column_condition.as_dict()
            for related_column_condition in result['related_column_conditions'] or []
        ]
        return result


class UsernameToProjectIds(BaseModel):
    """
    An object containing a username and the list of project ids they have access to.

    :type username: :class:`str`
    :param username: Username

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: List of project ids
    """

    _fields = ['username', 'project_ids']

    def __init__(self, username=None, project_ids=None):
        self.username = username
        self.project_ids = project_ids


class RelatedColumnCondition(BaseModel):
    """
    Class representing a related column condition of a :class:`LimitingCondition` object.

    :type assay_type_name: :class:`str`
    :param assay_type_name: Assay type name

    :type value: :class:`str`
    :param value: Value

    :type value_type: :class:`~ldclient.enums.QueryConditionValueType`
    :param value_type: Value type.

    :type value_inverted: :class:`bool`
    :param value_inverted: True if the value is inverted, False otherwise
    """

    _fields = ['assay_type_name', 'value', 'value_type', 'value_inverted']

    def __init__(self, assay_type_name=None, value=None, value_type=None, value_inverted=None):
        self.assay_type_name = assay_type_name
        self.value = value
        self.value_type = value_type
        self.value_inverted = value_inverted


# move backup above assaytype
class Rationale(BaseModel):
    """
    Class representing a :class:`Rationale` object.

    :type description: :class:`str`
    :param description: The description of the rationale.

    :type user_name: :class:`str`
    :param user_name: The user that owns the rationale.

    :type corporate_id: :class:`str`
    :param corporate_id: Identifies the compound the rationale is mapped with.

    :type live_report_id: :class:`int`
    :param live_report_id: The LiveReport that owns the rationale.

    :type default: :class:`bool`
    :param default: Specifies whether or not the rationale is the default.

    :type id: :class:`str`
    :param id: The ID of the rationale.

    :type created_at: :class:`int`
    :param created_at: The timestamp that the rationale has been created.

    :type updated_at: :class:`int`
    :param updated_at: The timestamp that the rationale has been updated.
    """
    _fields = [
        'description', 'user_name', 'corporate_id', 'live_report_id', 'id', 'default', 'created_at',
        'updated_at'
    ]

    def __init__(self,
                 description="",
                 user_name="",
                 corporate_id=None,
                 live_report_id=None,
                 default=None,
                 id=None,
                 created_at=None,
                 updated_at=None):
        self.description = description
        self.user_name = user_name
        self.corporate_id = corporate_id
        self.live_report_id = live_report_id
        self.default = default
        self.id = id
        self.created_at = created_at
        self.updated_at = updated_at


class Database(BaseModel):
    """
    Class representing the database source.

    :type id: :class:`str`
    :param id: The ID of the specific data source.

    :type name: :class:`str`
    :param name: The name of the database source.

    :type display_name: :class:`str`
    :param display_name: The display name of the database source.

    :type projects: :class:`list` of :class:`str`
    :param projects: The list of projects using the data source.

    :type primary: :class:`bool`
    :param primary: Whether or not this is the primary data source. (default is True)

    :type writeback: :class:`bool`
    :param writeback: Whether or not the data source supports the LiveDesign import feature. (default is True)

    :type ideas: :class:`bool`
    :param ideas: Whether or not the data source supports ideas. (default is True)
    """
    _fields = ['id', 'display_name', 'name', 'projects', 'ideas', 'writeback', 'primary']

    def __init__(self,
                 name,
                 projects=None,
                 ideas=True,
                 primary=True,
                 writeback=True,
                 id=None,
                 display_name=None):
        self.id = id
        self.name = name
        self.display_name = display_name
        self.projects = projects
        self.primary = primary
        self.writeback = writeback
        self.ideas = ideas


class DatabaseColumn(BaseModel):
    """
    Class representing the :class:`DatabaseColumn` object.

    :type id: :class:`str`
    :param id: The specific identifier for the DatabaseColumn object.

    :type name: :class:`str`
    :param name: The name for the database column.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier that the database column is mapped.

    :type value_type: :class:`str`
    :param value_type: What type of data can be stored in the column. Options are:

        -"integer"
        -"float"
        -"string"
        -"pdb_data"
        -"rationale"
        -"molecule"
        -"date"
        -"datetime"
        -"attachment"
        -"3D"
        -"variable" (Meant as a palceholder for Formula and New Assays)
        -"image"
        -"boolean"

    :type folder_path: :class:`str`
    :param folder_path: The folder path for the database column.
    """
    _fields = ['id', 'name', 'addable_column_id', 'value_type', 'folder_path']

    def __init__(self, id, name, addable_column_id, value_type, folder_path):
        self.id = id
        self.name = name
        self.addable_column_id = addable_column_id
        self.value_type = value_type
        self.folder_path = folder_path


class LiveReport(BaseModel):
    """
    Class representing the :class:`LiveReport` object.

    :type title: :class:`str`
    :param title: The shown name for the LiveReport.

    :type description: :class:`str`
    :param description: A description about the LiveReport.

    :type update_policy: :class:`str`
    :param update_policy: Specification of when the LiveReport is updated. Options are:

        -"never"
        -"by_cachebuilder" (default)
        -"when_opened"

    :type template: :class:`bool`
    :param template: Whether or not the LiveReport is a template.

    :type shared_editable: :class:`bool`
    :param shared_editable: Whether or not the LiveReport is able to be editable between shared groups.

    :type default_rationale: :class:`str`
    :param default_rationale: The default rationale.

    :type assay_view: :class:`dict`
    :param assay_view: The view of the LiveReport to be displayed.

    :type id: :class:`str`
    :param id: The unique identification of the LiveReport.

    :type alias: :class:`str`
    :param alias: .. deprecated:: 9.0.1 An alias for the LiveReport.

    :type project_id: :class:`str`
    :param project_id: The project that the LiveReport is a part of. (The default is 0)

    :type owner: :class:`str`
    :param owner: The group that owns the LiveReport.

    :type is_private: :class:`bool`
    :param is_private: Whether or not the LiveReport is private.

    :type active: :class:`bool`
    :param active: Whether or not the LiveReport is active.

    :type addable_columns: :class:`list` of :class:`str`
    :param addable_columns: A list of the addable columns in the LiveReport.

    :type tags: :class:`list` of :class:`str`
    :param tags: A list of the tags for the LiveReport.

    :type last_saved_date: :class:`int`
    :param last_saved_date: A timestamp of the last time that the LiveReport was saved.

    :type sorted_columns: :class:`list` of :class:`dict`
    :param sorted_columns: The columns by which the rows are sorted from highest to lowest priority, as well as whether each column is ascending or descending. It is a list of dicts referencing addable column ids and a boolean of whether or not it is ascending.

    :type frozen_rows: :class:`list` of :class:`str`
    :param frozen_rows: A list of the frozen rows in the LiveReport.

    :type hidden_rows: :class:`list` of :class:`str`
    :param hidden_rows: A list of the hidden rows in the LiveReport.

    :type scaffolds: :class:`list` of :class:`dict`
    :param scaffolds: A list of the scaffolds in the LiveReport. The dictionary has the id and name, both in :class:`str` format.

    :type report_level: :class:`str`
    :param report_level: The report level that determines what rows should appear for each compound in a LiveReport. The data provided is not dependent on the number of rows. Options are:

        -"PARENT" (creates one row per Corporate ID) (default)
        -"PARENT_BATCH (creates one row per Corporate_ID + lot and ignores POSES)
        -"PARENT_SALT (creates one row per Corporate_ID + salt and ignores POSES)
        -"PARENT_BATCH_SALT(creates one row per corporate_ID + lot + salt and should be the same number of rows as PARENT_BATCH)
        -"PARENT_BATCH_EXPERIMENT (creates one row per lot per experiment on experimental columns defined in the LiveReport)
        -"POSE" (creates one row per pose, which is the same as PARENT if there are no poses)

    :type experiment_column_id: :class:`str`
    :param experiment_column_id: The experiment column id.

    :type type: :class:`str`
    :param type: Specifies whether the LiveReport is designed for normal drug discovery or for designating reactants and fragments. Options are:

        -"COMPOUND" (default)
        -"REACTANT"
        -"DEVICE"
    """
    NEVER = 'never'
    BY_CACHEBUILDER = 'by_cachebuilder'
    WHEN_OPENED = 'when_opened'
    COMPOUND = 'compound'
    REACTANT = 'reactant'
    DEVICE = 'device'

    _fields = [
        'title', 'description', 'update_policy', 'template', 'shared_editable', 'default_rationale',
        'assay_view', 'id', 'alias', 'project_id', 'owner', 'is_private', 'active',
        'addable_columns', 'tags', 'last_saved_date', 'sorted_columns', 'hidden_rows',
        'frozen_rows', 'scaffolds', 'report_level', 'experiment_column_id', 'type'
    ]

    def __init__(self,
                 title,
                 description='',
                 update_policy=BY_CACHEBUILDER,
                 template=False,
                 shared_editable=True,
                 default_rationale=None,
                 assay_view=None,
                 id=None,
                 alias=None,
                 project_id=0,
                 owner=None,
                 is_private=False,
                 active=True,
                 addable_columns=None,
                 tags=None,
                 last_saved_date=None,
                 sorted_columns=None,
                 frozen_rows=None,
                 hidden_rows=None,
                 scaffolds=None,
                 report_level='parent',
                 experiment_column_id=None,
                 type=COMPOUND):
        self.title = title
        self.description = description
        self.update_policy = update_policy
        self.type = type
        self.template = template
        self.shared_editable = shared_editable
        self.default_rationale = default_rationale
        self.assay_view = assay_view
        self.id = id
        self.alias = alias
        if alias:
            warnings.warn('The alias field is deprecated and will soon be removed from LD.',
                          DeprecationWarning)
        self.project_id = str(project_id)
        self.owner = owner
        self.is_private = is_private
        self.active = active
        self.addable_columns = addable_columns
        self.tags = tags if tags else []
        self.last_saved_date = last_saved_date
        self.sorted_columns = sorted_columns if sorted_columns else []
        self.frozen_rows = frozen_rows if frozen_rows else []
        self.hidden_rows = hidden_rows if hidden_rows else []
        self.scaffolds = scaffolds if scaffolds else []
        self.report_level = report_level
        self.experiment_column_id = experiment_column_id

    def as_json_string(self, standard_required=True):
        """
        Returns the LiveReport as a JSON string.

        :type standard_required: :class:`bool`
        :param standard_required: A boolean to determine whether or not all LiveReport parameters should be displayed in the dictionary. If false, empty strings, lists, values that are false or 0 are omitted. (Default is true)

        :rtype: :class:`str`
        :return: Returns a JSON string of the LiveReport.
        """
        return json.dumps(self.as_dict(standard_required=standard_required))

    # standard_required determines whether the field should always have an entry in the dict.  This is useful for fields
    # which are required to represent a live_report, compared to those which add supplementary details
    def as_dict(self, standard_required=True):
        """
        Returns the LiveReport as a dictionary.

        :type standard_required: :class:`bool`
        :param standard_required: A boolean to determine whether or not all LiveReport parameters should be displayed in the dictionary. If false, empty strings, lists, values that are false or 0 are omitted. (Default is true)

        :rtype: :class:`dict`
        :return: Returns a dictionary of the LiveReport.
        """
        data = {}
        if self.title or standard_required:
            data['title'] = self.title
        if self.description or standard_required:
            data['description'] = self.description
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.last_saved_date or standard_required:
            data['last_saved_date'] = self.last_saved_date
        if self.sorted_columns or standard_required:
            data['sorted_columns'] = self.sorted_columns
        if self.frozen_rows or standard_required:
            data['frozen_rows'] = self.frozen_rows
        if self.hidden_rows or standard_required:
            data['hidden_rows'] = self.hidden_rows
        if self.scaffolds or standard_required:
            data['scaffolds'] = self.scaffolds
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.addable_columns:
            data['addable_columns'] = self.addable_columns
        if self.tags or standard_required:
            data['tags'] = self.tags
        if self.update_policy or standard_required:
            data['update_policy'] = self.update_policy
        if self.type or standard_required:
            data['type'] = self.type
        if self.template or standard_required:
            data['template'] = self.template
        if self.is_private or standard_required:
            data['is_private'] = self.is_private
        if self.active or standard_required:
            data['active'] = self.active
        if self.shared_editable or standard_required:
            data['shared_editable'] = self.shared_editable
        if self.default_rationale:
            data['default_rationale'] = self.default_rationale
        if self.assay_view:
            data['assay_view'] = self.assay_view.as_dict()
        if self.id or standard_required:
            data['id'] = self.id
        if self.report_level or standard_required:
            data['report_level'] = self.report_level
        if self.alias:
            data['alias'] = self.alias
        if self.project_id:
            data['project_id'] = self.project_id
        if self.experiment_column_id:
            data['experiment_column_id'] = self.experiment_column_id
        return data

    @staticmethod
    def from_dict(data):
        """
        Returns a LiveReport object from the data provided.

        :type data: :class:`dict`
        :param data: A dictionary of all of the LiveReport fields.

        :rtype: :class:`LiveReport`
        :return: Returns a LiveReport object from the data provided.
        """
        addable_columns = data.get('addable_columns')
        alias = data.get('alias')

        if data.get('assay_view'):
            assay_view = ViewMap.from_dict(data['assay_view'])
        else:
            assay_view = None

        default_rationale = data.get('default_rationale')

        return LiveReport(
            title=data['title'],
            description=data['description'],
            owner=data['owner'],
            is_private=data['is_private'],
            update_policy=data['update_policy'],
            type=data['type'],
            template=data['template'],
            active=data['active'],
            shared_editable=data['shared_editable'],
            addable_columns=addable_columns,
            tags=data['tags'],
            last_saved_date=data['last_saved_date'],
            sorted_columns=data['sorted_columns'],
            frozen_rows=data['frozen_rows'],
            hidden_rows=data['hidden_rows'],
            default_rationale=default_rationale,
            assay_view=assay_view,
            id=data['id'],
            alias=alias,
            project_id=data.get('project_id'),
            scaffolds=data.get('scaffolds'),
            report_level=data.get('report_level'),
            experiment_column_id=data.get('experiment_column_id'),
        )

    @classmethod
    def from_list(cls, arr):
        """
        Returns a list of LiveReport objects from the data provided.

        :type arr: :class:`list` of :class:`dict`
        :param arr: A list of dictionaries of various LiveReports to be created.

        :rtype: :class:`list` of :class:`LiveReport`
        :return: Returns a list of LiveReports.
        """
        return [cls.from_dict(elem) for elem in arr]


class LiveReportMetadata(BaseModel):
    """
    Class representing the :class:`LiveReportMetadata` object.

    :type title: :class:`str`
    :param title: The shown name for the LiveReport.

    :type description: :class:`str`
    :param description: A description about the LiveReport.

    :type update_policy: :class:`str`
    :param update_policy: Specification of when the LiveReport is updated. Options are:

        -"never"
        -"by_cachebuilder" (default)
        -"when_opened"

    :type template: :class:`bool`
    :param template: Whether or not the LiveReport is a template.

    :type shared_editable: :class:`bool`
    :param shared_editable: Whether or not the LiveReport is able to be editable between shared groups.

    :type id: :class:`str`
    :param id: The unique identification of the LiveReport.

    :type alias: :class:`str`
    :param alias: An alias for the LiveReport.

    :type project_id: :class:`str`
    :param project_id: The project that the LiveReport is a part of. The default is 0.

    :type owner: :class:`str`
    :param owner: The group that owns the LiveReport.

    :type is_private: :class:`bool`
    :param is_private: Whether or not the LiveReport is private.

    :type active: :class:`bool`
    :param active: Whether or not the LiveReport is active.

    :type tags: :class:`list` of :class:`str`
    :param tags: A list of the tags for the LiveReport.

    :type last_saved_date: :class:`int`
    :param last_saved_date: A timestamp of the last time that the LiveReport was saved.

    :type report_level: :class:`str`
    :param report_level: The report level that determines what rows should appear for each compound in a LiveReport. Options are:

        -"PARENT" (creates one row per Corporate ID) (default)
        -"PARENT_BATCH (creates one row per Corporate_ID + lot and ignores POSES)
        -"PARENT_SALT (creates one row per Corporate_ID + salt and ignores POSES)
        -"PARENT_BATCH_SALT(creates one row per corporate_ID + lot + salt and should be the same number of rows as PARENT_BATCH)
        -"PARENT_BATCH_EXPERIMENT (creates one row per lot per experiment on experimental columns defined in the LiveReport)
        -"POSE" (creates one row per pose, which is the same as PARENT if there are no poses)

    :type experiment_column_id: :class:`str`
    :param experiment_column_id: The experiment column id.

    :type type: :class:`str`
    :param type: Specifies whether the LiveReport is designed for normal drug discovery or for designating reactangs and fragments. Options are:

        -"COMPOUND" (default)
        -"REACTANT"
        -"DEVICE"
    """

    _fields = [
        'title', 'description', 'update_policy', 'template', 'shared_editable', 'id', 'alias',
        'project_id', 'owner', 'is_private', 'active', 'tags', 'last_saved_date', 'report_level',
        'type'
    ]

    NEVER = 'never'
    BY_CACHEBUILDER = 'by_cachebuilder'
    WHEN_OPENED = 'when_opened'
    COMPOUND = 'compound'
    REACTANT = 'reactant'

    def __init__(self,
                 title,
                 description='',
                 update_policy=BY_CACHEBUILDER,
                 template=False,
                 shared_editable=True,
                 id=None,
                 alias=None,
                 project_id=0,
                 owner=None,
                 is_private=False,
                 active=True,
                 tags=None,
                 last_saved_date=None,
                 report_level='parent',
                 type=COMPOUND):
        self.title = title
        self.description = description
        self.update_policy = update_policy
        self.type = type
        self.template = template
        self.shared_editable = shared_editable
        self.id = id
        self.alias = alias
        if alias:
            warnings.warn('The alias field is deprecated and will soon be removed from LD.',
                          DeprecationWarning)
        self.project_id = str(project_id)
        self.owner = owner
        self.is_private = is_private
        self.active = active
        self.tags = tags if tags else []
        self.last_saved_date = last_saved_date
        self.report_level = report_level

    def as_json_string(self, standard_required=True):
        """
        Returns the LiveReport Metadata as a JSON string.

        :type standard_required: :class:`bool`
        :param standard_required: A boolean to determine whether the field should always have an entry in the dictionary.

        :rtype: :class:`str`
        :return: Returns a JSON string of the LiveReport Metadata.
        """
        return json.dumps(self.as_dict(standard_required=standard_required))

    def as_dict(self, standard_required=True):
        """
        Returns the LiveReport Metadata as a dictionary.

        :type standard_required: :class:`bool`
        :param standard_required: A boolean to determine whether the field should always have an entry in the dictionary.

        :rtype: :class:`dict`
        :return: Returns a dictionary of the LiveReport Metadata.
        """
        data = {}
        if self.title or standard_required:
            data['title'] = self.title
        if self.description or standard_required:
            data['description'] = self.description
        if self.owner or standard_required:
            data['owner'] = self.owner
        if self.last_saved_date or standard_required:
            data['last_saved_date'] = self.last_saved_date
        if self.tags or standard_required:
            data['tags'] = self.tags
        if self.update_policy or standard_required:
            data['update_policy'] = self.update_policy
        if self.type or standard_required:
            data['type'] = self.type
        if self.template or standard_required:
            data['template'] = self.template
        if self.is_private or standard_required:
            data['is_private'] = self.is_private
        if self.active or standard_required:
            data['active'] = self.active
        if self.shared_editable or standard_required:
            data['shared_editable'] = self.shared_editable
        if self.id or standard_required:
            data['id'] = self.id
        if self.report_level or standard_required:
            data['report_level'] = self.report_level
        if self.alias:
            data['alias'] = self.alias
        if self.project_id:
            data['project_id'] = self.project_id
        return data

    @staticmethod
    def from_dict(data):
        """
        Returns a LiveReport Metadata object from the data provided.

        :type data: :class:`dict`
        :param data: A dictionary of all of the LiveReport Metadata fields.

        :rtype: :class:`LiveReport`
        :return: Returns a LiveReport Metadata object from the data provided.
        """

        alias = data.get('alias')

        return LiveReportMetadata(title=data['title'],
                                  description=data['description'],
                                  owner=data['owner'],
                                  is_private=data['is_private'],
                                  update_policy=data['update_policy'],
                                  type=data['type'],
                                  template=data['template'],
                                  active=data['active'],
                                  shared_editable=data['shared_editable'],
                                  tags=data['tags'],
                                  last_saved_date=data['last_saved_date'],
                                  id=data['id'],
                                  alias=alias,
                                  project_id=data.get('project_id'),
                                  report_level=data.get('report_level'))

    @classmethod
    def from_list(cls, arr):
        """
        Returns a list of LiveReport Metadata objects from the data provided.

        :type arr: :class:`list` of :class:`dict`
        :param arr: A list of dictionaries of various LiveReport Metadata objects to be created.

        :rtype: :class:`list` of :class:`LiveReportMetadata`
        :return: Returns a list of LiveReport Metadata objects.
        """
        return [cls.from_dict(elem) for elem in arr]


class ModelRecursive(BaseModel):
    """
    A class that tags data with how it should be inherited from parent models.

    :type tag: :class:`~ldclient.enums.RecursiveTag`
    :param tag: A tag part of a tagged union of the generic recursive structure.

    :type value: :class:`object`
    :param value: An arbitrary value.
    """
    _fields = ['tag', 'value']

    def __init__(self, tag, value=None):
        self.tag = tag
        self.value = value


class ModelCommand(BaseModel):
    """
    A single Command to be executed as part of a model.

    :type id: :class:`str`
    :param id: The identifier for the model command.

    :type command: :class:`str`
    :param command: The command to be accomplished.

    :type driver_id: :class:`str`
    :param driver_id: The driver id for the command.
    """
    _fields = ['id', 'command', 'driver_id']

    def __init__(self, command, driver_id, id=None):
        self.id = id
        self.command = command
        self.driver_id = driver_id


class ModelAsyncDriver(BaseModel):
    """
    A Driver for handling Async Commands on Task Engine.

    :type id: :class:`str`
    :param id: The identifier for the model async driver.

    :type name: :class:`str`
    :param name: The name for the model async driver.

    :type job_id_regex: :class:`str`
    :param job_id_regex: Regex to find the job id.

    :type job_status: :class:`str`
    :param job_status: The job status for the command.

    :type job_kill_command: :class:`str`
    :param job_kill_command: The job kill command for the model async driver.
    """
    _fields = ['id', 'name', 'job_id_regex', 'job_status', 'job_kill_command']

    def __init__(self, name, job_id_regex, job_status, id=None, job_kill_command=None):
        self.id = id
        self.name = name
        self.job_id_regex = job_id_regex
        self.job_status = job_status
        self.job_kill_command = job_kill_command


class ModelReturn(BaseModel):
    """
    A model predictions object.

    :type key: :class:`str`
    :param key: The key for the model return.

    :type type: :class:`~ldclient.enums.ModelReturnType`
    :param type: The type that will be returned

    :type units: :class:`str`
    :param units: The units for the return type.

    :type display_name: :class:`str`
    :param display_name: The display name for the object.

    :type tag: :class:`~ldclient.enums.RecursiveTag`
    :param tag: A tag part of a tagged union of the generic recursive structure

    :type precision: :class:`int`
    :param precision: The precision for values that will be returned. (Default is 1)

    :type addable_column_id: :class:`str`
    :param addable_column_id: The identifier for the addable column id.

    :type id: :class:`str`
    :param id: The identifier for the ModelReturn.
    """

    _fields = [
        'id', 'key', 'type', 'units', 'display_name', 'tag', 'precision', 'addable_column_id'
    ]

    def __init__(self,
                 key,
                 type,
                 units,
                 display_name,
                 tag='DEFAULT',
                 precision=1,
                 addable_column_id=None,
                 id=None):
        self.key = key
        self.type = type
        self.units = units
        self.tag = tag
        self.precision = precision
        self.display_name = display_name
        self.addable_column_id = addable_column_id
        self.id = id


class Model(BaseModel):
    """
    Models, Parametrized Models and Protocols all use the same structure and table.

    :type name: :class:`str`
    :param name: The name of the model.

    :type archived: :class:`bool`
    :param archived: Whether or not the model is archived.

    :type published: :class:`bool`
    :param published: Whether or not the model is published.

    :type user: :class:`str`
    :param user: The user that created the model.

    :type folder: :class:`str`
    :param folder: The location of the model.

    :type returns: :class:`list` of :class:`ModelReturn`
    :param returns: A list of what the model will return.

    :type project_ids: :class:`list` of :class:`int`
    :param project_ids: A list of what projects use the model.

    :type template_vars: :class:`list` of :class:`ModelTemplateVar`
    :param template_vars: A list of template variables

    :type description: :class:`str`
    :param description: A description of the model.

    :type parent: :class:`int`
    :param parent: The identifier for the parent model.

    :type commands: :class:`list` of :class:`ModelCommand`
    :param commands: A list of commands

    :type batch_size: :class:`ModelRecursive`
    :param batch_size: The processing size of each model.

    :type command_type: :class:`ModelRecursive`
    :param command_type: The type of command to be run

    :type command_queue: :class:`ModelRecursive`
    :param command_queue: Specifies the task engine queue system

    :type created_at: :class:`int`
    :param created_at: The timestamp that the model was created at.

    :type updated_at: :class:`int`
    :param updated_at: The timestamp that the model was updated at.

    :type id: :class:`str`
    :param id: The identifier for the model.

    :type automatic_rerun: :class:`ModelRecursive`
    :param automatic_rerun: True if the model should be rerun automatically and false if manually.

    :type as_merged: :class:`Model`
    :param as_merged: Model object with resolved parameter values.
    """
    _fields = [
        'id', 'parent', 'name', 'archived', 'published', 'user', 'created_at', 'updated_at',
        'folder', 'returns', 'template_vars', 'batch_size', 'description', 'command_type',
        'command_queue', 'commands', 'project_ids', 'automatic_rerun', 'as_merged'
    ]

    def __init__(self,
                 name,
                 archived,
                 published,
                 user,
                 folder,
                 returns,
                 project_ids,
                 template_vars,
                 description,
                 parent=None,
                 commands=None,
                 batch_size=None,
                 command_type=None,
                 command_queue=None,
                 created_at=None,
                 updated_at=None,
                 id=None,
                 automatic_rerun=None,
                 as_merged=None):
        self.id = id
        self.parent = parent
        self.name = name
        self.archived = archived
        self.published = published
        self.user = user
        self.created_at = created_at
        self.updated_at = updated_at
        self.folder = folder
        self.returns = returns
        self.template_vars = template_vars
        self.batch_size = batch_size
        self.description = description
        self.command_type = command_type
        self.command_queue = command_queue
        self.commands = [] if not commands else commands
        self.project_ids = ensure_string_list(project_ids) if project_ids else []
        self.automatic_rerun = automatic_rerun
        self.as_merged = as_merged

    def serialize_commands(self):
        """
        Serialize the commands as a dictionary

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of commands, in dictionary form, for the current model.
        """
        return [cmd.as_dict() for cmd in self.commands]

    def serialize_returns(self):
        """
        Serialize the returns as a dictionary

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of returns, in dictionary form, for the current model.
        """
        return [ret.as_dict() for ret in self.returns]

    def serialize_template_vars(self):
        """
        Serialize the template variables as a dictionary

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of template variables, in dictionary form, for the current model.
        """
        if not self.template_vars:
            return []
        return [var.as_dict() for var in self.template_vars]

    def serialize_batch_size(self):
        """
        Serialize the batch size as a dictionary

        :rtype: :class:`dict`
        :return: Returns the batch size, in dictionary form, for the current model.
        """
        if self.batch_size:
            return self.batch_size.as_dict()
        return None

    def serialize_command_queue(self):
        """
        Serialize the command queue as a dictionary

        :rtype: :class:`dict`
        :return: Returns the command queue, in dictionary form, for the current model.
        """
        if self.command_queue:
            return self.command_queue.as_dict()
        return None

    def serialize_command_type(self):
        """
        Serialize the command type as a dictionary

        :rtype: :class:`dict`
        :return: Returns the command type, in dictionary form, for the current model.
        """
        if self.command_type:
            return self.command_type.as_dict()
        return None

    def serialize_as_merged(self):
        """
        Serialize the as_merged as a dictionary

        :rtype: :class:`dict`
        :return: Returns the Model with resolved values in dictionary form.
        """
        if self.as_merged:
            return self.as_merged.as_dict()
        return None

    @staticmethod
    def deserialize_commands(cmds):
        """
        Deserializes the given commands from dictionary form to a list of :class:`ModelCommand`.

        :type cmds: :class:`dict`
        :param cmds: A dictionary of commands

        :rtype: :class:`list` of :class:`ModelCommand`
        :return: A list of model commands.
        """
        return [ModelCommand.from_dict(cmd) for cmd in cmds]

    @staticmethod
    def deserialize_returns(rets):
        """
        Deserializes the given returns from dictionary form to a list of :class:`ModelReturn`.

        :type rets: :class:`dict`
        :param rets: A dictionary of returns

        :rtype: :class:`list` of :class:`ModelReturn`
        :return: A list of model returns.
        """
        return [ModelReturn.from_dict(ret) for ret in rets]

    @staticmethod
    def deserialize_template_vars(vars):
        """
        Deserializes the given template variables from dictionary form to a list of :class:`ModelTemplateVar`.

        :type vars: :class:`dict`
        :param vars: A dictionary of template variables

        :rtype: :class:`list` of :class:`ModelTemplateVar`
        :return: A list of model template variables.
        """
        return [ModelTemplateVar.from_dict(var) for var in vars]

    @staticmethod
    def deserialize_batch_size(d):
        """
        Deserializes the given batch size.

        :type d: :class:`dict`
        :param d: A dictionary of batch size

        :rtype: :class:`ModelRecursive`
        :return: The batch size.
        """
        if d is None:
            return None
        return ModelRecursive.from_dict(d)

    @staticmethod
    def deserialize_command_queue(d):
        """
        Deserializes the given command queue.

        :type d: :class:`dict`
        :param d: A dictionary of command queue

        :rtype: :class:`ModelRecursive`
        :return: The command queue.
        """
        if d is None:
            return None
        return ModelRecursive.from_dict(d)

    @staticmethod
    def deserialize_command_type(d):
        """
        Deserializes the given command type.

        :type d: :class:`dict`
        :param d: A dictionary of command type

        :rtype: :class:`ModelRecursive`
        :return: The command type.
        """
        if d is None:
            return None
        return ModelRecursive.from_dict(d)

    @staticmethod
    def deserialize_as_merged(d):
        """
        Deserializes as_merged.

        :type d: :class:`dict`
        :param d: A dictionary of as_merged

        :rtype: :class:`Model`
        :return: The Model with resolved values.
        """
        if d is None:
            return None
        return Model.from_dict(d)


class ModelTemplateVar(BaseModel):
    """
    A binding of a variable in a command. This is a class for the :class:`ModelTemplateVar` object.

    :type id: :class:`str`
    :param id: The identifier for the model template variable.

    :type tag: :class:`~ldclient.enums.RecursiveTag`
    :param tag: A tag part of a tagged union of the generic recursive structure

    :type type: :class:`~ldclient.enums.ModelTemplateVarType`
    :param type: The type of the variables

    :type name: :class:`str`
    :param name: The name for the model template variable.

    :type data: :class:`str`
    :param data: The data for the model template variable.

    :type is_optional: :class:`bool`
    :param is_optional: Whether or not the variable is optional

    :type optional_parameter_name: :class:`str`
    :param optional_parameter_name: The name of the optional parameter
    """
    _fields = ['id', 'tag', 'type', 'name', 'data', 'is_optional', 'optional_parameter_name']

    def __init__(self,
                 tag,
                 type,
                 name,
                 data=None,
                 id=None,
                 is_optional=False,
                 optional_parameter_name=None):
        self.id = id
        self.tag = tag
        self.type = type
        self.name = name
        self.data = data
        self.is_optional = is_optional
        self.optional_parameter_name = optional_parameter_name


class QuickTestModel(BaseModel):
    """
    This is a class for the :class:`QuickTestModel` object.

    :type model: :class:`Model`
    :param model: The model for the quick test model.

    :type protocol: :class:`Model`
    :param protocol: The protocol for the quick test model.

    :type compounds: :class:`list` of :class:`str`
    :param compounds: A list of SMILES strings to run the model on.

    :type files: :class:`dict`
    :param files: A dictionary to map filenames to the contents of a file.

    :type column_types: :class:`dict`
    :param column_types: A dictionary for the types of columns.

    :type compound_data: :class:`dict` of :class:`dict`
    :param compound_data: Maps smiles strings to a map from property to value. For example: {'c': {'Mol Wt': '16', 'hac': '1'}, 'cc': {'Mol Wt': '30', 'hac': '2'}}.

    :type project_id: :class:`str`
    :param project_id: The project id for the quick test model.

    :type task_engine_task_id: :class:`str`
    :param task_engine_task_id: The task engine id for the quick test model.

    :type request_id: :class:`str`
    :param request_id: The request id for the model.
    """
    _fields = [
        'model',
        'protocol',
        'compounds',
        'files',
        'column_types',
        'compound_data',
        'project_id',
        'task_engine_task_id',
        'request_id',
    ]

    def __init__(self,
                 model,
                 protocol,
                 compounds,
                 files,
                 column_types,
                 compound_data,
                 project_id,
                 task_engine_task_id=None,
                 request_id=None):
        self.model = model
        self.protocol = protocol
        self.compounds = compounds
        self.files = files
        self.column_types = column_types
        self.compound_data = compound_data
        self.project_id = project_id
        self.task_engine_task_id = task_engine_task_id
        self.request_id = request_id
        self.serialize_model = lambda: self.model.as_dict()
        self.serialize_protocol = lambda: self.protocol.as_dict()


class Folder(BaseModel):
    """
    :type name: :class:`str`
    :param name: The name of the folder.

    :type id: :class:`str`
    :param id: The identifier for the folder.

    :type project_id: :class:`str`
    :param project_id: The project id that the folder is located inside.
    """
    _fields = ['name', 'id', 'project_id']

    def __init__(self, name, id, project_id):
        """
        Represents a LiveReport folder within a project. Also known as a Tag.
        """
        self.name = name
        self.id = id
        self.project_id = project_id


class ColumnDescriptor(BaseModel):
    """
    A column descriptor defines how the column is going to appear to the user in the LR.
    A column descriptor may also contain column filters and coloring rules.

    :type column_id: :class:`str`
    :param column_id: The column id for the descriptor.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column id for the descriptor.

    :type active_mpo_id_for_coloring: :class:`str`
    :param active_mpo_id_for_coloring: The active Mpo ID used for the coloring rules.

    :type live_report_id: :class:`str`
    :param live_report_id: The LiveReport id that the column descriptor takes effect in.

    :type display_name: :class:`str`
    :param display_name: The display name for the descriptor.

    :type width: :class:`int`
    :param width: The width for the column descriptor.

    :type color_styles: :class:`list` of :class:`ColorStyle`
    :param color_styles: The list of color styles for the column descriptor.

    :type filters: :class:`list` of :class:`ColumnFilter`
    :param filters: The list of column filters for the column descriptor.

    :type hidden: :class:`bool`
    :param hidden: Whether or not the entire column is hidden.

    :type aggregation_type: :class:`~ldclient.enums.CellAggregationType`
    :param aggregation_type: Defines the strategy for aggregating values in assay column cells. The options are:

    :type id: :class:`str`
    :param id: The id for the column descriptor.
    """
    MEDIAN = 'median'
    ARITHMETIC_MEAN = 'arithmetic_mean'
    MIN = 'min'
    MAX = 'max'
    GEOMETRIC_MEAN = 'geometric_mean'
    SYSTEM_DEFAULT = 'system_default'
    STD_DEV = 'std_dev'
    COUNT = 'count'

    _fields = [
        'column_id', 'addable_column_id', 'active_mpo_id_for_coloring', 'live_report_id',
        'display_name', 'width', 'color_styles', 'filters', 'hidden', 'aggregation_type', 'id'
    ]

    def __init__(self,
                 column_id,
                 addable_column_id=None,
                 active_mpo_id_for_coloring=None,
                 live_report_id=None,
                 display_name=None,
                 width=None,
                 color_styles=None,
                 filters=None,
                 hidden=False,
                 aggregation_type=SYSTEM_DEFAULT,
                 id=None):
        self.column_id = column_id
        if column_id and not addable_column_id:
            warnings.warn(
                'The column_id field is deprecated and will soon be removed from LD. '
                'Please also include addable_column_id to ease code migration when column_id will be removed.',
                DeprecationWarning)
        self.addable_column_id = addable_column_id
        self.live_report_id = live_report_id
        if display_name:
            warnings.warn(
                'The display_name field has been removed from LD and addable_column_id should be used instead.',
                DeprecationWarning)
        self.display_name = display_name if display_name else column_id
        self.width = width
        self.color_styles = color_styles if color_styles else []
        self.filters = filters if filters else []
        self.hidden = hidden
        self.aggregation_type = aggregation_type
        self.id = id
        self.active_mpo_id_for_coloring = active_mpo_id_for_coloring


class ColumnFilter(BaseModel):
    """
    :type filter_type: :class:`str`
    :param filter_type: The type of filter being used. Options are:

        -"range"
        -"structure"
        -"text"
        -"relative_date"
        -"row"

    :type enabled: :class:`bool`
    :param enabled: Whether or not the column filter is enabled.

    :type inverted: :class:`bool`
    :param inverted: Whether or not the column filter is inverted.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column id for the column filter.
    """
    _fields = ['filter_type', 'enabled', 'inverted']

    def __init__(self, filter_type='text', enabled=True, inverted=False, addable_column_id=None):
        self.filter_type = filter_type
        self.enabled = enabled
        self.inverted = inverted
        self.addable_column_id = addable_column_id

    @classmethod
    def parse_from_dict(cls, data):
        """
        Returns the type of filter from a dictionary.

        :type data: :class:`dict`
        :param data: A dictionary for a type of Column Filter object.

        :rtype: :class:`TextColumnFilter` or :class:`RangeColumnFilter` or :class:`ValueError`
        :return: Returns a type of ColumnFilter or throws a :class:`ValueError`.
        """
        if data['filter_type'] == 'text':
            return TextColumnFilter.from_dict(data)
        elif data['filter_type'] == 'range':
            return RangeColumnFilter.from_dict(data)
        else:
            raise ValueError("filter_type must be either 'text' or 'range'")


class TextColumnFilter(ColumnFilter):
    """
    :type text_match_type: :class:`str`
    :param text_match_type: How the text match type is matched. Options are:

        -"exactly"
        -"prefix"
        -"anywhere"

    :type case_sensitive: :class:`bool`
    :param case_sensitive: Whether or not the filter is case sensitive.

    :type delimiter: :class:`str`
    :param delimiter: The delimiter for the text filter search.

    :type value: :class:`str`
    :param value: The text column filter value.
    """
    _fields = ColumnFilter._fields + ['text_match_type', 'case_sensitive', 'delimiter', 'value']

    def __init__(self,
                 filter_type='text',
                 enabled=True,
                 inverted=False,
                 addable_column_id=None,
                 text_match_type='exactly',
                 case_sensitive=False,
                 delimiter='__DEBUG__',
                 value=None):
        ColumnFilter.__init__(self,
                              filter_type=filter_type,
                              enabled=enabled,
                              inverted=inverted,
                              addable_column_id=addable_column_id)
        self.text_match_type = text_match_type
        self.case_sensitive = case_sensitive
        self.delimiter = delimiter
        self.value = value
        # Validation
        valid_text_match_types = ['exactly', 'prefix', 'anywhere']
        if self.text_match_type not in valid_text_match_types:
            raise ValueError("text_match_type must be one of: {}".format(
                ', '.join(valid_text_match_types)))
        if value is not None and type(value) is not str:
            self.value = str(value)


class RangeColumnFilter(ColumnFilter):
    """
    :type date: :class:`bool`
    :param date: Whether or not the values being filtered are dates.

    :type log_scale: :class:`bool`
    :param log_scale: Whether to increment or decrement the range sliders in log scale.

    :type range_high: `int`
    :param range_high: The high range of the filter.

    :type range_low: `int`
    :param range_low: The low range of the filter.
    """
    _fields = ColumnFilter._fields + ['date', 'log_scale', 'range_high', 'range_low']

    def __init__(self,
                 filter_type='range',
                 enabled=True,
                 inverted=False,
                 addable_column_id=None,
                 date=False,
                 log_scale=False,
                 range_high=None,
                 range_low=None):
        ColumnFilter.__init__(self,
                              filter_type=filter_type,
                              enabled=enabled,
                              inverted=inverted,
                              addable_column_id=addable_column_id)
        self.date = date
        self.log_scale = log_scale
        self.range_high = range_high
        self.range_low = range_low


class Parameter(BaseModel):
    """
    :type operator: :class:`str`
    :param operator: The type of operator to be set as a parameter.

    :type value: :class:`str`
    :param value: The value for the operator.

    :type runtime: :class:`int`
    :param runtime: The runtime for the parameter.

    :type display: :class:`str`
    :param display: The display for the parameter.
    """
    _fields = ['operator', 'value', 'runtime', 'display']

    def __init__(self, operator, value, runtime, display=None):
        self.operator = operator
        self.value = value
        self.runtime = runtime
        self.display = display


class ViewMap(BaseModel):
    """
    :type color_styles: :class:`list` of :class:`ColorStyle`
    :param color_styles: The list of color styles to be displayed in the view map.
    """

    _fields = ['color_styles']

    def __init__(self, color_styles):
        self.color_styles = color_styles if color_styles else []

    def serialize_color_styles(self):
        """
        Seralize the color styles.

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of color styles, each as a dictionary.
        """
        return [style.as_dict() for style in self.color_styles]

    @staticmethod
    def deserialize_color_styles(styles):
        """
        Deseralizes the color styles from a list

        :type styles: :class:`list` of :class:`dict`
        :param styles: A list of styles to be deserialized

        :rtype: :class:`list` of :class:`ColorStyle`
        :return: Returns a list of color styles.
        """
        return ColorStyle.from_list(styles)


class ColorStyle(BaseModel):
    """
    :type color_low: :class:`str`
    :param color_low: The color of the low.

    :type color_high: :class:`str`
    :param color_high: The color of the high.

    :type range_low: :class:`int`
    :param range_low: The lower range limit.

    :type range_high: :class:`int`
    :param range_high: The upper range limit.

    :type value_low: :class:`int`
    :param value_low: The lower value limit.

    :type value_high: :class:`int`
    :param value_high: The higher value limit.

    :type log_scale: :class:`bool`
    :param log_scale: Whether or not to use a log scale for the color styles.

    :type matching_string: :class:`str`
    :param matching_string: The matching string.

    :type relative: :class:`bool`
    :param relative: Relative flag of color style.

    :type color_style_type: :class:`str`
    :param color_style_type: The color style type, options are:

        -"NUMBER"
        -"DATE"
        -"CATEGORICAL"
    """
    _fields = [
        'color_low',
        'color_high',
        'range_low',
        'range_high',
        'value_low',
        'value_high',
        'log_scale',
        'matching_string',
        'relative',
        'color_style_type',
    ]

    deserialize_color_style_type = ColorStyleType.from_str

    def __init__(self,
                 color_low,
                 color_high=None,
                 range_low=None,
                 range_high=None,
                 value_low=None,
                 value_high=None,
                 log_scale=False,
                 matching_string=None,
                 relative=False,
                 color_style_type=None):
        self.color_low = color_low
        self.color_high = color_high
        self.range_low = range_low
        self.range_high = range_high
        self.value_low = value_low
        self.value_high = value_high
        self.log_scale = log_scale
        self.matching_string = matching_string
        self.relative = relative
        self.color_style_type = color_style_type


class LiveReportResult(BaseModel):
    """
    The result of executing a LiveReport.

    :type columns: :class:`list` of :class:`str`
    :param columns: The list of columns.

    :type results: :class:`list` of :class:`str`
    :param results: The results of the LiveReport.
    """

    _fields = ['columns', 'results']

    def __init__(self, columns, results):
        self.columns = columns
        self.results = results


class FreeformColumn(BaseModel):
    """
    Class representing a freeform column.

    :type name: :class:`str`
    :param name: Display name of the freeform column.

    :type description: :class:`str`
    :param description: Description of the freeform column.

    :type type: :class:`str`
    :param type: Type of column. Valid values:

        - `FreeformColumn.COLUMN_TEXT`
        - `FreeformColumn.COLUMN_NUMBER`
        - `FreeformColumn.COLUMN_BOOLEAN` (not supported if `picklist` is True)
        - `FreeformColumn.COLUMN_DATE`
        - `FreeformColumn.COLUMN_ATTACHMENT` (not supported if `picklist` is True)

    :type picklist: :class:`bool`
    :param picklist: True if the freeform column has a picklist of values, False otherwise.

    :type values: :class:`list` of :class:`FreeformColumnPicklistValue`
    :param values: List of the values available in the picklist. Empty list if the freeform column is not a picklist.

    :type published: :class:`bool`
    :param published: True if the freeform column is published.

    :type project_id: :class:`str`
    :param project_id: The numeric project ID in which the freeform column exists.

    :type live_report_id: :class:`str`
    :param live_report_id: If published is False, this is the LiveReport ID to which the column is restricted.
        NOTE: The column must manually be added to the LiveReport.

    :type value_type: :class:`str`
    :param value_type: Type of column. This is safe to set to `None`.  If not, this should agree with the `type` field.

    :type id: :class:`str`
    :param id: The ID of the freeform column.

    :type multiple_values_allowed: :class:`bool`
    :param multiple_values_allowed: True if multiple values are allowed, False otherwise.

    :type read_only: :class:`bool`
    :param read_only: True if freeformcolumn is read only, False otherwise.

    :type edit_access_users: :class:`list` of :class:`int`
    :param edit_access_users: List of the ids of users who are allowed to edit freeformcolumn cell values when freeform is read only.
        Empty list if no user is given explicit access to edit the freeformcolumn when in read only mode.
    """
    COLUMN_TEXT = "TEXT"
    COLUMN_NUMBER = "NUMBER"
    COLUMN_BOOLEAN = "BOOLEAN"
    COLUMN_DATE = "DATE"
    COLUMN_ATTACHMENT = "ATTACHMENT"

    VALUE_STRING = "string"

    _fields = [
        'id', 'name', 'description', 'type', 'value_type', 'published', 'live_report_id',
        'project_id', 'picklist', 'values', 'multiple_values_allowed', 'read_only', 'edit_access_users'
    ]

    def __init__(self,
                 name,
                 description,
                 type=COLUMN_TEXT,
                 picklist=False,
                 read_only=False,
                 edit_access_users=[],
                 values=[],
                 published=None,
                 project_id=None,
                 live_report_id=None,
                 value_type=VALUE_STRING,
                 id=None,
                 multiple_values_allowed=None):
        picklist_values = [
            FreeformColumnPicklistValue(
                value=value) if not isinstance(value, FreeformColumnPicklistValue) else value
            for value in values
        ]

        self.id = id
        self.name = name
        self.description = description
        self.type = type
        self.value_type = value_type
        self.published = published
        self.live_report_id = live_report_id
        self.project_id = project_id
        self.picklist = picklist
        self.values = picklist_values
        self.multiple_values_allowed = multiple_values_allowed
        self.read_only = read_only
        self.edit_access_users = edit_access_users

    def serialize_values(self):
        return FreeformColumnPicklistValue.as_list(self.values)

    @staticmethod
    def deserialize_values(values):
        return FreeformColumnPicklistValue.from_list(values)


class FreeformColumnPicklistValue(BaseModel):
    """
    Class representing a value in the picklist of a freeform column.

    :type id: :class:`str`
    :param id: ID of the picklist value.

    :type value: :class:`str`
    :param value: The picklist value.
    """
    _fields = [
        'id',
        'value',
    ]

    def __init__(self, value, id=None):
        self.id = id
        self.value = value


class Observation(BaseModel):
    """
    Class representing an observation on an entity. For example, this could be an experimental value or a freeform
    column value paired with its corresponding entity ID.

    :type project_id: :class:`str`
    :param project_id: Numeric ID of the project that the observation is in.

    :type entity_id: :class:`str`
    :param entity_id: ID of the entity that the observation is on.

    :type addable_column_id: :class:`str`
    :param addable_column_id: ID of the addable column that the observation belongs to.

    :type value: :class:`str`
    :param value: Value of the observation.

    :type live_report_id: :class:`str`
    :param live_report_id: Numeric ID of the LiveReport that the observation is in.

    :type units: :class:`str`
    :param units: Units.

    :type lot_number: :class:`str`
    :param lot_number: Lot number.

    :type salt_id: :class:`str`
    :param salt_id: Salt ID.

    :type protocol: :class:`str`
    :param protocol: ID of the protocol.

    :type date: :class:`str`
    :param date: Date of the observation.

    :type notebook: :class:`str`
    :param notebook: Notebook.

    :type notebook_page: :class:`str`
    :param notebook_page: Notebook page.

    :type concentration: :class:`float`
    :param concentration: Concentration.

    :type concentration_units: :class:`str`
    :param concentration_units: Concentration units.

    :type operator: :class:`str`
    :param operator: Operator.

    :type batch: :class:`str`
    :param batch: Batch.

    :type attachment_urls: :class:`list` of :class:`dict`
    :param attachment_urls: List of attachment URLs.

    :type published: :class:`bool`
    :param published: True if observation is published, False otherwise.

    :type id: :class:`str`
    :param id: ID of the observation.
    """
    _fields = [
        'id',
        'project_id',
        'entity_id',
        'addable_column_id',
        'value',
        'units',
        'lot_number',
        'salt_id',
        'protocol',
        'date',
        'notebook',
        'notebook_page',
        'concentration',
        'concentration_units',
        'operator',
        'batch',
        'attachment_urls',
        'live_report_id',
        'published',
    ]

    def __init__(self,
                 project_id,
                 entity_id,
                 addable_column_id,
                 value,
                 live_report_id=None,
                 units=None,
                 lot_number=None,
                 salt_id=None,
                 protocol=None,
                 date=None,
                 notebook=None,
                 notebook_page=None,
                 concentration=None,
                 concentration_units=None,
                 operator=None,
                 batch=None,
                 attachment_urls=None,
                 published=None,
                 id=None):
        self.id = id
        self.project_id = project_id
        self.entity_id = entity_id
        self.addable_column_id = addable_column_id
        self.value = value
        self.units = units
        self.lot_number = lot_number
        self.salt_id = salt_id
        self.protocol = protocol
        self.date = date
        self.notebook = notebook
        self.notebook_page = notebook_page
        self.concentration = concentration
        self.concentration_units = concentration_units
        self.operator = operator
        self.batch = batch
        self.attachment_urls = attachment_urls
        self.live_report_id = live_report_id
        self.published = published


class CompoundLink(BaseModel):
    """
    Represents a compound link search result object.

    :type id: :class:`str`
    :param id: The identifier for the compound link search result object.

    :type real_entity_id: :class:`str`
    :param real_entity_id: The real entity ID for the resulting compound link.

    :type virtual_entity_id: :class:`str`
    :param virtual_entity_id: The virtual entity ID for the resulting compound link.
    """

    _fields = ['id', 'real_entity_id', 'virtual_entity_id']

    def __init__(self, id=None, real_entity_id=None, virtual_entity_id=None):
        self.id = id
        self.real_entity_id = real_entity_id
        self.virtual_entity_id = virtual_entity_id


class FormulationData(BaseModel):
    """
    Represents the result of a formulations search.

    This object does not correspond to any object in the database;
    rather, it is a specific object designed to transmit a "dump"
    of the formulations data in a project of LiveReport.

    :type entity_id: str
    :param entity_id: string entity ID of the formulation

    :type aliases: :class:`list` of :class:`str`
    :param aliases: a list of string aliases of this formulation

    :type units: str
    :param units: the units of the formulation

    :type components: :class:`list` of :class:`FormulationDataComponent`
    :param components: list of components
    """

    _fields = ['entity_id', 'aliases', 'units', 'components']

    def __init__(self, entity_id=None, aliases=None, units=None, components=None):
        self.entity_id = entity_id
        self.aliases = aliases or []
        self.units = units
        self.components = components or []

    @staticmethod
    def deserialize_components(components_json):
        """
        Deserialize the formulation data components from a list.

        :type components_json: :class:`list` of :class:`dict`
        :param components_json: A list of dictionaries to be deserialized into formulation data components.

        :rtype: :class:`FormulationDataComponent`
        :return: Returns a list of formulation data component objects.
        """
        return FormulationDataComponent.from_list(components_json)

    def serialize_components(self):
        """
        Serializes formulation data components from the components of the current formulation data.

        :rtype: :class:`list` of :class:`dict`
        :return: Returns a list of formulation data components with each component as a dictionary.
        """
        return FormulationDataComponent.as_list(self.components)


class FormulationDataComponent(BaseModel):
    """
    Represents a formulations data component. Formulations have,
    in general, multiple of these.


    :type functional_type: :class:`str`
    :param functional_type: the functional type of the component

    :type name: :class:`str`
    :param name: the name of the component

    :type quantity: :class:`float`
    :param quantity: the quantity of the component

    """

    _fields = ['functional_type', 'name', 'quantity']

    def __init__(self, functional_type=None, name=None, quantity=None):
        self.functional_type = functional_type
        self.name = name
        self.quantity = quantity


class FormulationResult(BaseModel):
    """
    An object containing a formulation entity id and it's recipe

    :type entity_id: :class:`str`
    :param entity_id: the entity id of the formulation

    :type recipe: :class:`str`
    :param recipe: the recipe of the formulation
    """

    _fields = ['entity_id', 'recipe']

    def __init__(self, entity_id=None, recipe=None):
        self.entity_id = entity_id
        self.recipe = recipe


class ColumnAlias(BaseModel):
    """
    Represents a Column Alias search result object.

    :type id: :class:`str`
    :param id: The identifier of rthe current column alias object.

    :type alias: :class:`str`
    :param alias: The alias name of the column alias object.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column identifier of the column alias object.

    :type live_report_id: :class:`str`
    :param live_report_id: The LiveReport id for the column alias object.

    :type project_id: :class:`str`
    :param project_id: The project identifier for the column alias object.
    """

    _fields = ['id', 'alias', 'addable_column_id', 'live_report_id', 'project_id']

    def __init__(self,
                 id=None,
                 alias=None,
                 addable_column_id=None,
                 live_report_id=None,
                 project_id=None):
        self.id = id
        self.alias = alias
        self.addable_column_id = addable_column_id
        self.live_report_id = live_report_id
        self.project_id = project_id


class ImportResponse(BaseModel):
    """
    Represents a newly imported entity.

    :type corporate_id: :class:`str`
    :param corporate_id: The corporate id for the import response.

    :type ligand_id: :class:`str`
    :param ligand_id: The ligand id for the import response.

    :type protein_id: :class:`str`
    :param protein_id: The protein id for the import repsonse.

    :type messages: :class:`list` of :class:`str`
    :param messages: The list of messages from the import response.

    :type success: :class:`bool`
    :param success: Whether or not the import was successful or not.

    :type observations_imported: :class:`int`
    :param observations_imported: The number of observations imported.
    """
    _fields = [
        'corporate_id', 'ligand_id', 'protein_id', 'messages', 'success', 'observations_imported'
    ]

    def __init__(self,
                 corporate_id=None,
                 ligand_id=None,
                 protein_id=None,
                 messages=None,
                 success=None,
                 observations_imported=None):
        self.corporate_id = corporate_id
        self.ligand_id = ligand_id
        self.protein_id = protein_id
        self.messages = messages
        self.success = success
        self.observations_imported = observations_imported


class User(BaseModel):
    """
    Represents a LiveDesign user & its permissions.

    :type id: :class:`str`
    :param id: ID of the user.

    :type username: :class:`str`
    :param username: The user's username.

    :type decimal_separator: :class:`str`
    :param decimal_separator: The character used to separate decimals for numbers displayed to this user.

    :type plexus_visible: :class:`bool`
    :param plexus_visible: Whether or not the user is visible to plexus. Note: plexus_visible is now deprecated and will be removed in future versions

    :type can_build_protocols: :class:`bool`
    :param can_build_protocols: Whether or not the user has permissions to create protocols.

    :type is_admin: :class:`bool`
    :param is_admin: Whether or not the user is an admin.

    :type is_project_admin: :class:`bool`
    :param is_project_admin: Whether or not the user is an admin of a project.

    :type can_use_admin_panel: :class:`bool`
    :param can_use_admin_panel: Whether or not the user has permissions to use the admin panel.

    :type last_login: :class:`int`
    :param last_login: Epoch time of the last time the user logged into the LiveDesign server.

    :type administered_project_ids: :class:`list` of :class:`str`
    :param administered_project_ids: List of (numeric) IDs of projects that this user is admin for.
    """
    _fields = [
        'id', 'username', 'decimal_separator', 'plexus_visible', 'can_build_protocols', 'is_admin',
        'is_project_admin', 'can_use_admin_panel', 'last_login', 'administered_project_ids'
    ]

    def __init__(self,
                 id=None,
                 username=None,
                 decimal_separator=None,
                 plexus_visible=None,
                 can_build_protocols=None,
                 is_admin=None,
                 is_project_admin=None,
                 can_use_admin_panel=None,
                 last_login=None,
                 administered_project_ids=None):
        self.id = id
        self.username = username
        self.decimal_separator = decimal_separator
        self.plexus_visible = plexus_visible
        self.can_build_protocols = can_build_protocols
        self.is_admin = is_admin
        self.is_project_admin = is_project_admin
        self.can_use_admin_panel = can_use_admin_panel
        self.last_login = last_login
        self.administered_project_ids = administered_project_ids


class RawGroup(BaseModel):
    """
    Represents a user group.

    :type id: :class:`str`
    :param id: The numeric ID of the group.

    :type name: :class:`str`
    :param name: The display name of the group.
    """
    _fields = ['id', 'name']

    def __init__(self, id=None, name=None):
        self.id = id
        self.name = name


class RawGroupPermission(BaseModel):
    """
    Represents a group's permission on a particular project.

    :type id: :class:`str`
    :param id: The unique identifier of the group-project permission pair.

    :type group_id: :class:`str`
    :param group_id: The ID of the group with permission.

    :type project_id: :class:`str`
    :param project_id: The ID of the project on which the group has permission.
    """
    _fields = ['id', 'group_id', 'project_id']

    def __init__(self, id=None, group_id=None, project_id=None):
        self.id = id
        self.group_id = group_id
        self.project_id = project_id


class RawGroupMembership(BaseModel):
    """
    Represents a user's membership within a user group.

    :type id: :class:`str`
    :param id: Unique identifier for the user-group membership pair.

    :type user_id: :class:`str`
    :param user_id: ID of the user holding the membership.

    :type group_id: :class:`str`
    :param group_id: ID of the group of which the user is a member.
    """
    _fields = ['id', 'user_id', 'group_id']

    def __init__(self, id=None, user_id=None, group_id=None):
        self.id = id
        self.user_id = user_id
        self.group_id = group_id


class StructureAttachment(BaseModel):
    """
    Represents a structure attachment.

    :type id: :class:`str`
    :param id: Unique identifier of the structure attachment.

    :type structure_transformation: :class:`~ldclient.enums.StructureTransformation`
    :param structure_transformation: The type of transformation of the structure data.

    :type attachment_alternate_id: :class:`str`
    :param attachment_alternate_id: alternate id of the structure attachment.
    """
    _fields = ['id', 'structure_transformation', 'attachment_alternate_id']

    def __init__(self, id=None, structure_transformation=None, attachment_alternate_id=None):
        self.id = id
        self.structure_transformation = structure_transformation
        self.attachment_alternate_id = attachment_alternate_id

    @staticmethod
    def deserialize_structure_transformation(structure_transformation):
        return StructureTransformation.from_str(structure_transformation)


class Structure(BaseModel):
    """
    Represents a structure. This could be a ligand or a protein, for example.

    :type structure_attachments: :class:`list` of :class:`StructureAttachment`
    :param structure_attachments: The list of attachments for this structure.

    :type entity_id: :class:`str`
    :param entity_id: Corporate ID of the entity.
    """
    _fields = ['structure_attachments', 'entity_id']

    def __init__(self, structure_attachments=None, entity_id=None):
        self.structure_attachments = structure_attachments
        self.entity_id = entity_id

    def serialize_structure_attachments(self):
        return [
            structure_attachment.as_dict() for structure_attachment in self.structure_attachments
        ]

    @staticmethod
    def deserialize_structure_attachments(structure_attachments):
        return StructureAttachment.from_list(structure_attachments)


class Complex(BaseModel):
    """
    Represents a complex, also known as a pose.

    :type id: :class:`str`
    :param id: Unique identifier of the complex.

    :type protein: :class:`Structure`
    :param protein: The protein portion of the complex.

    :type ligand: :class:`Structure`
    :param ligand: The ligand portion of the complex.

    :type column_id: :class:`str`
    :param column_id: The addable column ID the complex belongs to.

    :type project_id: :class:`str`
    :param project_id: The project ID the complex belongs to.

    :type published: :class:`bool`
    :param published: Whether or not the complex is available outside of its project.

    :type is_favorite: :class:`bool`
    :param is_favorite: Whether or not the complex is a favorite.

    :type pldb_integration_code: :class:`str`
    :param pldb_integration_code: PLDB integration code.

    :type pose_format: :class:`~ldclient.enums.PoseFormat`
    :param pose_format: The file format of the pose data.

    :type ligand_name: :class:`str`
    :param: Display name of the ligand.

    :type target_name: :class:`str`
    :param target_name: Display name of the target.
    """
    _fields = [
        'id', 'protein', 'ligand', 'column_id', 'project_id', 'published', 'is_favorite',
        'pldb_integration_code', 'pose_format', 'ligand_name', 'target_name'
    ]

    def __init__(self,
                 id=None,
                 protein=None,
                 ligand=None,
                 column_id=None,
                 project_id=None,
                 published=None,
                 is_favorite=None,
                 pldb_integration_code=None,
                 pose_format=None,
                 ligand_name=None,
                 target_name=None):
        self.id = id
        self.protein = protein
        self.ligand = ligand
        self.column_id = column_id
        self.project_id = project_id
        self.published = published
        self.is_favorite = is_favorite
        self.pldb_integration_code = pldb_integration_code
        self.pose_format = pose_format
        self.ligand_name = ligand_name
        self.target_name = target_name

    def serialize_protein(self):
        return self.protein.as_dict()

    @staticmethod
    def deserialize_protein(protein):
        return Structure.from_dict(protein)

    def serialize_ligand(self):
        return self.ligand.as_dict()

    @staticmethod
    def deserialize_ligand(ligand):
        return Structure.from_dict(ligand)

    @staticmethod
    def deserialize_pose_format(pose_format):
        return PoseFormat.from_str(pose_format)


class PersistedAssay(BaseModel):
    """
    Represents an assay returned by :meth:`~ldclient.LDClient.get_or_create_assay`.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column ID of the assay.

    :type assay_name: :class:`str`
    :param assay_name: The name of the assay.

    :type assay_type_name: :class:`str`
    :param assay_type_name: The name of the assay type.

    :type column_type: :class:`~ldclient.enums.AssayColumnType`
    :param column_type: The type of assay column.

    :type project_ids: :class:`list` of :class:`str`
    :param project_ids: The IDs of projects in which the assay is visible.
    """
    _fields = ['addable_column_id', 'assay_name', 'assay_type_name', 'column_type', 'project_ids']

    def __init__(self,
                 addable_column_id=None,
                 assay_name=None,
                 assay_type_name=None,
                 column_type=None,
                 project_ids=None):
        self.addable_column_id = addable_column_id
        self.assay_name = assay_name
        self.assay_type_name = assay_type_name
        self.column_type = column_type
        self.project_ids = project_ids

    @staticmethod
    def deserialize_column_type(column_type):
        return AssayColumnType.from_str(column_type)


class AttachmentMetadata(BaseModel):
    """
    Represents an attachment, i.e. image, .mae file, etc.

    :type id: :class:`str`
    :param id: Unique identifier of the attachment.

    :type file_name: :class:`str`
    :param file_name: The name of the attached file.

    :type file_type: :class:`~ldclient.enums.AttachmentType`
    :param file_type: The type of attachment.

    :type project_id: :class:`list` of :class:`str`
    :param project_id: List of project IDs in which the attachment is visible.

    :type updated_at: :class:`int`
    :param updated_at: The last date the attachment was updated, in epoch time.

    :type created_at: :class:`int`
    :param created_at: The date the attachment was created, in epoch time.

    :type hash: :class:`str`
    :param hash: The hash of the attachment.

    :type projects: :class:`list` of :class:`int`
    :param projects: **DEPRECATED**: Please use project_id instead.
    """
    _fields = [
        'id', 'file_name', 'file_type', 'project_id', 'updated_at', 'created_at', 'hash', 'projects'
    ]

    def __init__(self,
                 id=None,
                 file_name=None,
                 file_type=None,
                 project_id=None,
                 updated_at=None,
                 created_at=None,
                 hash=None,
                 projects=None):
        self.id = id
        self.file_name = file_name
        self.file_type = file_type
        self.project_id = project_id
        self.updated_at = updated_at
        self.created_at = created_at
        self.hash = hash
        self.projects = projects

    @staticmethod
    def deserialize_file_type(file_type):
        return AttachmentType.from_str(file_type)


class Property(BaseModel):
    """
    Represents a property/config setting on the server.

    :type key: :class:`str`
    :param key: The key, or property name.

    :type value: :class:`str`
    :param value: The current set value for the property.

    :type default_value: :class:`str`
    :param default_value: The default value for the property.

    :type supported_values: :class:`str`
    :param supported_values: The supported values for the property.

    :type description: :class:`str`
    :param description: A description of the property.

    :type category: :class:`~ldclient.enums.PropertyCategory`
    :param category: The category the property belongs to.

    :type sub_category: :class:`~ldclient.enums.PropertySubCategory`
    :param sub_category: The subcategory the property belongs to.

    :type acl_level: :class:`~ldclient.enums.PropertyAclLevel`
    :param acl_level: The ACL level of the property.

    :type managed_by: :class:`~ldclient.enums.PropertyManagedBy`
    :param managed_by: Who or what the property is managed by.

    :type frontend: :class:`bool`
    :param frontend: Whether or not the property is for front-end code.

    :type requires_restart: :class:`bool`
    :param requires_restart: Whether or not restarting the server is required for changes to this property to take
        effect.
    """
    _fields = [
        'key', 'value', 'default_value', 'supported_values', 'description', 'category',
        'sub_category', 'acl_level', 'managed_by', 'frontend', 'requires_restart'
    ]

    def __init__(self,
                 key=None,
                 value=None,
                 default_value=None,
                 supported_values=None,
                 description=None,
                 category=None,
                 sub_category=None,
                 acl_level=None,
                 managed_by=None,
                 frontend=None,
                 requires_restart=None):
        self.key = key
        self.value = value
        self.default_value = default_value
        self.supported_values = supported_values
        self.description = description
        self.category = category
        self.sub_category = sub_category
        self.acl_level = acl_level
        self.managed_by = managed_by
        self.frontend = frontend
        self.requires_restart = requires_restart

    @staticmethod
    def deserialize_category(category):
        return PropertyCategory.from_str(category)

    @staticmethod
    def deserialize_sub_category(sub_category):
        return PropertySubCategory.from_str(sub_category)

    @staticmethod
    def deserialize_acl_level(acl_level):
        return PropertyAclLevel.from_str(acl_level)

    @staticmethod
    def deserialize_managed_by(managed_by):
        return PropertyManagedBy.from_str(managed_by)


class MPO(BaseModel):
    """
    Represents a Multi-Parameter Optimization (MPO) column.

    :type name: :class:`str`
    :param name: The displayed name of the MPO.

    :type project_id: :class:`str`
    :param project_id: The LiveDesign Project ID that this MPO is associated with.

    :type constituents: :class:`list` of :class:`MPOConstituent`
    :param constituents: List of the constituent columns

    :type description: :class:`str`
    :param description: User-facing description of the MPO.

    :type active: :class:`bool`
    :param active: Whether the MPO is active (True) or archived (False).

    :type id: :class:`str`
    :param id: The ID of the MPO.

    :type addable_column_id: :class:`str`
    :param addable_column_id: The addable column ID of the MPO column.

    :type only_calc_if_complete: :class:`bool`
    :param only_calc_if_complete: Whether an MPO score should be calculated only when all constituent columns have values (True) or should be calculated even when some constituents are empty (False). Defaults to False.

    :type creator: :class:`str`
    :param creator: The username of the user that created the MPO.
    """
    _fields = [
        'id', 'addable_column_id', 'name', 'description', 'active', 'only_calc_if_complete',
        'project_id', 'constituents', 'creator'
    ]

    def __init__(self,
                 name,
                 project_id,
                 constituents,
                 description=None,
                 active=True,
                 id=None,
                 addable_column_id=None,
                 only_calc_if_complete=False,
                 creator=None):
        self.id = id
        self.name = name
        self.project_id = project_id
        self.constituents = constituents
        self.description = description
        self.active = active
        self.addable_column_id = addable_column_id
        self.only_calc_if_complete = only_calc_if_complete
        self.creator = creator

    def serialize_constituents(self):
        return MPOConstituent.as_list(self.constituents)

    @staticmethod
    def deserialize_constituents(constituents):
        return MPOConstituent.from_list(constituents)


class MPOConstituent(BaseModel):
    """
    Represents a MPO constituent, including the addable column, weighting, and threshold settings

    :type addable_column_id: :class:`str`
    :param addable_column_id: The ID of the LiveDesign addable column that this constituent tracks.

    :type type: :class:`~ldclient.enums.MPOConstituentType`
    :param type: The constituent profile type: MONO, BELL, or CATEGORY. Only the corresponding attribute should be set (i.e. mono_threshold for type: MONO)

    :type addable_column_name: :class:`str`
    :param addable_column_name: Name of LD addable column, derived from the Addable Column ID.

    :type mono_threshold: :class:`~ldclient.models.MPOConstituentMonoThreshold`
    :param mono_threshold: The constituent profile for a mono-type numeric constituent.

    :type bell_threshold: :class:`~ldclient.models.MPOConstituentBellThreshold`
    :param bell_threshold: The constituent profile for a bell-type numeric consituent.

    :type category: :class:`~ldclient.models.MPOConstituentCategory`
    :param category: The constituent profile for a category-type constituent.

    :type weight: :class:`float`
    :param weight: The relative weighting of this constituent in the overall MPO score. Defaults to 1.0.
    """
    _fields = [
        'addable_column_id', 'addable_column_name', 'type', 'mono_threshold', 'bell_threshold',
        'category', 'weight'
    ]

    def __init__(self,
                 addable_column_id,
                 type,
                 addable_column_name=None,
                 mono_threshold=None,
                 bell_threshold=None,
                 category=None,
                 weight=1.0):
        self.addable_column_id = addable_column_id
        self.addable_column_name = addable_column_name
        self.type = type
        self.mono_threshold = mono_threshold
        self.bell_threshold = bell_threshold
        self.category = category
        self.weight = weight

    def serialize_mono_threshold(self):
        return self.mono_threshold.as_dict() if self.mono_threshold else None

    def serialize_bell_threshold(self):
        return self.bell_threshold.as_dict() if self.bell_threshold else None

    def serialize_category(self):
        return self.category.as_dict() if self.category else None

    @staticmethod
    def deserialize_mono_threshold(mono_threshold):
        return MPOConstituentMonoThreshold.from_dict(mono_threshold) if mono_threshold else None

    @staticmethod
    def deserialize_bell_threshold(bell_threshold):
        return MPOConstituentBellThreshold.from_dict(bell_threshold) if bell_threshold else None

    @staticmethod
    def deserialize_category(category):
        return MPOConstituentCategory.from_dict(category) if category else None


class MPOConstituentMonoThreshold(BaseModel):
    """
    Represents a mono-type MPO constituent profile

    :type low_threshold: :class:`float`
    :param low_threshold: The low threshold value for the constituent property.

    :type high_threshold: :class:`float`
    :param high_threshold: The high threshold value for the constituent property.

    :type high_good: :class:`bool`
    :param high_good: Whether the MPO should assign high scores to values towards the high threshold (True) or towards the low threshold (False).
    """
    _fields = ['low_threshold', 'high_threshold', 'high_good']

    def __init__(self, low_threshold, high_threshold, high_good):
        self.low_threshold = low_threshold
        self.high_threshold = high_threshold
        self.high_good = high_good


class MPOConstituentBellThreshold(BaseModel):
    """
    Represents a bell-type MPO constituent profile

    :type low_threshold: :class:`float`
    :param low_threshold: The low threshold value for the constituent property.

    :type low_middle_threshold: :class:`float`
    :param low_middle_threshold: The low-middle threshold value for the constituent property.

    :type middle_high_threshold: :class:`float`
    :param middle_high_threshold: The middle-high threshold value for the constituent property.

    :type high_threshold: :class:`float`
    :param high_threshold: The high threshold value for the constituent property.

    :type middle_good: :class:`bool`
    :param middle_good: Whether the MPO should assign high scores to values in the middle of the bell curve defined by the thresholds (True) or low scores (False).
    """
    _fields = [
        'low_threshold', 'low_middle_threshold', 'middle_high_threshold', 'high_threshold',
        'middle_good'
    ]

    def __init__(self, low_threshold, low_middle_threshold, middle_high_threshold, high_threshold,
                 middle_good):
        self.low_threshold = low_threshold
        self.low_middle_threshold = low_middle_threshold
        self.middle_high_threshold = middle_high_threshold
        self.high_threshold = high_threshold
        self.middle_good = middle_good


class MPOConstituentCategory(BaseModel):
    """
    Represents a category-type MPO constituent profile. Individual / discrete values are marked as Good, Bad, or OK.
    This is the only MPO category available for text-type parameter columns, but it can be used for numeric-type columns too.

    :type good: :class:`list` of :class:`str`
    :param good: Values categorized as "good".

    :type bad: :class:`list` of :class:`str`
    :param bad: Values categorized as "bad".

    :type ok: :class:`list` of :class:`str`
    :param ok: Values categorized as "ok".
    """
    _fields = ['good', 'bad', 'ok']

    def __init__(self, good=None, bad=None, ok=None):
        self.good = good if good else []
        self.bad = bad if bad else []
        self.ok = ok if ok else []


class Formula(BaseModel):
    """
    Represents a LiveDesign Formula column.

    :type name: :class:`str`
    :param name: The name of the Formula column.

    :type expression: :class:`str`
    :param expression: Text expression of the formula's logic. LiveDesign columns are referenced by their addable column IDs. For a full list of functions available in formulas, please refer to the main LiveDesign documentation.

    :type project_id: :class:`str`
    :param project_id: The ID of the Project that this Formula is associated with.

    :type live_report_id: :class:`str`
    :param live_report_id: The ID of the LiveReport that this Formula was first created in.

    :type value_type: :class:`~ldclient.enums.ValueType`
    :param value_type: The value type of the Formula column, i.e. the datatype of the formula output.

    :type user: :class:`str`
    :param user: The creator and owner of the Formula. This controls who can edit the Formula.

    :type id: :class:`str`
    :param id: The addable column ID of the Formula column.

    :type published: :class:`bool`
    :param published: Whether the Formula is published to the Data & Columns tree (True) or local to a LiveReport (False). Defaults to False.

    :type created_at: :class:`int`
    :param created_at: The timestamp when the Formula was created.

    :type updated_at: :class:`int`
    :param updated_at: The timestamp when the Formula was last updated.

    :type description: :class:`str`
    :param description: The description of the Formula.

    """
    _fields = [
        'id', 'name', 'expression', 'project_id', 'live_report_id', 'value_type', 'published',
        'user', 'created_at', 'updated_at', 'description'
    ]

    def __init__(self,
                 name,
                 expression,
                 project_id,
                 live_report_id,
                 value_type,
                 user,
                 id=None,
                 published=False,
                 created_at=None,
                 updated_at=None,
                 description=None):
        self.id = id
        self.name = name
        self.expression = expression
        self.project_id = project_id
        self.live_report_id = live_report_id
        self.value_type = value_type
        self.published = published
        self.user = user
        self.created_at = created_at
        self.updated_at = updated_at
        self.description = description


class ProjectFavoriteColumn(BaseModel):
    """
    Represents a project favorite column

    :type id: :class:`str`
    :param id: id of the project favorite column

    :type project_id: :class:`str`
    :param project_id: id of the project for an addable column that is favorited

    :type column_id: :class:`str`
    :param column_id: id of the addable column that is favorited
    """
    _fields = ['id', 'project_id', 'column_id',]

    def __init__(self, id, project_id, column_id):
        self.id = id
        self.project_id = project_id
        self.column_id = column_id
