import json
from datetime import date, datetime
import logging
from enum import Enum
import uuid
import copy
from COMPS import Client
from COMPS.Data import Configuration, QueryCriteria, Simulation
from COMPS.Data.SerializableEntity import SerializableEntity, json_property, json_entity, parse_ISO8601_date, convert_if_string
from COMPS.Data.RelatableEntity import RelatableEntity
from COMPS.Data.TaggableEntity import TaggableEntity
from COMPS.Data.CommissionableEntity import CommissionableEntity
logger = logging.getLogger(__name__)
[docs]@json_entity()
class Experiment(TaggableEntity, CommissionableEntity, RelatableEntity, SerializableEntity):
"""
Represents a grouping of Simulations.
Contains various basic properties accessible by getters (and, in some cases, +setters):
* id
* +suite_id
* +name
* +description
* owner
* date_created
* last_modified
Also contains "child objects" (which must be specifically requested for retrieval using the
QueryCriteria.select_children() method of QueryCriteria):
* tags
* configuration
"""
def __init__(self, name, suite_id=None, description=None, configuration=None):
if not name:
raise RuntimeError('Experiment has not been initialized properly; non-null name required.')
self._id = None
self._name = name
self._suite_id = suite_id
self._description = description
self._owner = Client.auth_manager().username
self._date_created = None
self._last_modified = None
self._tags = None
self._configuration = configuration
self._is_dirty = None # these will be set in _register_change() below
self._is_config_dirty = None
self._register_change(config_changed=(configuration is not None))
@classmethod
def __internal_factory__(cls, id=None, name=None, suite_id=None, description=None, owner=None,
date_created=None, last_modified=None, tags=None, configuration=None):
exp = cls.__new__(cls)
exp._id = convert_if_string(id, uuid.UUID)
exp._name = name
exp._suite_id = convert_if_string(suite_id, uuid.UUID)
exp._description = description
exp._owner = owner
exp._date_created = convert_if_string(date_created, parse_ISO8601_date)
exp._last_modified = convert_if_string(last_modified, parse_ISO8601_date)
exp._tags = tags
if configuration:
if isinstance(configuration, Configuration):
exp._configuration = configuration
else:
config_json = Configuration.rest2py(configuration)
exp._configuration = Configuration(**config_json)
else:
exp._configuration = None
exp._is_dirty = False
exp._is_config_dirty = False
return exp
@json_property()
def id(self):
return self._id
@json_property()
def suite_id(self):
return self._suite_id
@suite_id.setter
def suite_id(self, suite_id):
self._suite_id = suite_id
self._register_change()
@json_property()
def name(self):
return self._name
@name.setter
def name(self, name):
self._name = name
self._register_change()
@json_property()
def description(self):
return self._description
@description.setter
def description(self, description):
self._description = description
self._register_change()
@json_property()
def owner(self):
return self._owner
@json_property()
def date_created(self):
return self._date_created
@json_property()
def last_modified(self):
return self._last_modified
@json_property()
def tags(self):
return self._tags # todo: immutable dict?
@json_property()
def configuration(self):
return self._configuration
@configuration.setter
def configuration(self, configuration):
self._configuration = configuration
self._register_change(config_changed=True)
########################
[docs] @classmethod
def get(cls, id=None, query_criteria=None):
"""
Retrieve one or more Experiments.
:param id: The id (str or UUID) of the Experiment to retrieve
:param query_criteria: A QueryCriteria object specifying basic property filters and tag-filters \
to apply to the set of Experiments returned, as well as which properties and child-objects to \
fill for the returned Experiments
:return: An Experiment or list of Experiments (depending on whether 'id' was specified) with \
basic properties and child-objects assigned as specified by 'query_criteria'
"""
if id and not isinstance(id, uuid.UUID):
try:
id = uuid.UUID(id)
except ValueError:
raise ValueError('Invalid id: {0}'.format(id))
qc_params = query_criteria.to_param_dict(Experiment) if query_criteria else {}
path = '/Experiments{0}'.format('/' + str(id) if id else '')
resp = Client.get(path
, params = qc_params)
json_resp = resp.json()
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug('Experiment Response:')
# logger.debug(json.dumps(json_resp, indent=4))
if 'Experiments' not in json_resp or \
( id is not None and len(json_resp['Experiments']) != 1 ):
logger.debug(json_resp)
raise RuntimeError('Malformed Experiments retrieve response!')
exps = []
for exp_json in json_resp['Experiments']:
exp_json = cls.rest2py(exp_json)
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug('Experiment:')
# logger.debug(json.dumps(exp_json, indent=4))
exp = Experiment.__internal_factory__(**exp_json)
exps.append(exp)
if id is not None:
return exps[0]
else:
return exps
[docs] def refresh(self, query_criteria=None):
"""
Update properties of an existing Experiment from the server.
:param query_criteria: A QueryCriteria object specifying which properties and child-objects \
to refresh on the Experiment
"""
if not self._id:
raise RuntimeError('Can\'t refresh an Experiment that hasn\'t been saved!')
exp = self.get(id=self.id, query_criteria=query_criteria)
# if exp.id: self._id = exp.id
if exp.name is not None: self._name = exp.name
if exp.suite_id is not None: self._suite_id = exp.suite_id
if exp.description is not None: self._description = exp.description
if exp.owner is not None: self._owner = exp.owner
if exp.date_created is not None: self._date_created = exp.date_created
if exp.last_modified is not None: self._last_modified = exp.last_modified
if exp.tags is not None: self._tags = exp.tags
if exp.configuration is not None: self._configuration = exp.configuration
[docs] def get_simulations(self, query_criteria=None):
"""
Retrieve Simulations contained in this Experiment.
:param query_criteria: A QueryCriteria object specifying basic property filters and tag-filters \
to apply to the set of Simulations returned, as well as which properties and child-objects to \
fill for the returned Simulations
:return: A list of Simulations with basic properties and child-objects assigned as specified \
by 'query_criteria'
"""
if not self._id:
raise RuntimeError('Invalid call to get_simulations(); Experiment hasn\'t been saved yet')
qc = copy.copy(query_criteria) if query_criteria else QueryCriteria()
qc = qc.where('experiment_id={0}'.format(str(self._id)))
return Simulation.get(query_criteria=qc)
[docs] def save(self):
"""
Save a single Experiment. If it's a new Experiment, an id is automatically assigned.
"""
if not self._is_dirty:
logger.info('Experiment has not been altered... no point in saving it!')
return
if not self._id:
exp_to_save = self
else:
exp_to_save = self.__internal_factory__(id=self._id,
name=self._name,
suite_id=self._suite_id,
description=self._description,
configuration=self._configuration if self._is_config_dirty else None)
save_exp = SerializableEntity.convertToDict(exp_to_save, include_hidden_props=True)
# indentval = 4 if logger.isEnabledFor(logging.DEBUG) else None
json_str = json.dumps({'Experiments': [ save_exp ] },
# indent=indentval,
default=lambda obj:
obj.isoformat() + '0Z' if isinstance(obj, (date, datetime))
else str(obj) if isinstance(obj, uuid.UUID)
else obj.name if isinstance(obj, Enum)
else obj)
resp = Client.post('/Experiments'
, data=json_str )
json_resp = resp.json()
if not self._id:
self._id = uuid.UUID(json_resp['Ids'][0])
self._is_dirty = False
self._is_config_dirty = False
def _register_change(self, config_changed=False):
if not self._is_dirty:
self._is_dirty = True
if config_changed and not self._is_config_dirty:
self._is_config_dirty = True