def test_dictionary_substitution(): """substitute_objects() should substitute LinkByUIDs that occur in dict keys and values.""" proc = ProcessRun("A process", uids={'id': '123'}) mat = MaterialRun("A material", uids={'generic id': '38f8jf'}) proc_link = LinkByUID.from_entity(proc) mat_link = LinkByUID.from_entity(mat) index = {(mat_link.scope.lower(), mat_link.id): mat, (proc_link.scope.lower(), proc_link.id): proc} test_dict = {LinkByUID.from_entity(proc): LinkByUID.from_entity(mat)} subbed = substitute_objects(test_dict, index) k, v = next((k, v) for k, v in subbed.items()) assert k == proc assert v == mat
def make_link(entity: BaseEntity): if len(entity.uids) == 0: raise ValueError("No UID for {}".format(entity)) elif native_uid and native_uid in entity.uids: return LinkByUID(native_uid, entity.uids[native_uid]) else: return LinkByUID.from_entity(entity)
def test_tuple_sub(): """substitute_objects() should correctly substitute tuple values.""" proc = ProcessRun('foo', uids={'id': '123'}) proc_link = LinkByUID.from_entity(proc) index = {(proc_link.scope, proc_link.id): proc} tup = (proc_link,) subbed = substitute_objects(tup, index) assert subbed[0] == proc
def test_scope_substitution(): """Test that the native id gets serialized, when specified.""" native_id = 'id1' # Create measurement and material with two ids mat = MaterialRun("A material", uids={ native_id: str(uuid4()), "an_id": str(uuid4()), "another_id": str(uuid4())}) meas = MeasurementRun("A measurement", material=mat, uids={ "some_id": str(uuid4()), native_id: str(uuid4()), "an_id": str(uuid4())}) # Turn the material pointer into a LinkByUID using native_id subbed = substitute_links(meas, scope=native_id) assert subbed.material == LinkByUID.from_entity(mat, scope=native_id) # Put the measurement into a list and convert that into a LinkByUID using native_id measurements_list = [meas] subbed = substitute_links(measurements_list, scope=native_id) assert subbed == [LinkByUID.from_entity(meas, scope=native_id)]
def test_template_access(): """A material run's template should be equal to its spec's template.""" template = MaterialTemplate("material template", uids={'id': str(uuid4())}) spec = MaterialSpec("A spec", uids={'id': str(uuid4())}, template=template) mat = MaterialRun("A run", uids=['id', str(uuid4())], spec=spec) assert mat.template == template mat.spec = LinkByUID.from_entity(spec) assert mat.template is None
def test_template_access(): """A process run's template should be equal to its spec's template.""" template = ProcessTemplate("process template", uids={'id': str(uuid4())}) spec = ProcessSpec("A spec", uids={'id': str(uuid4())}, template=template) proc = ProcessRun("A run", uids={'id': str(uuid4())}, spec=spec) assert proc.template == template proc.spec = LinkByUID.from_entity(spec) assert proc.template is None
def test_object_key_substitution(): """Test that client can copy a dictionary in which keys are BaseEntity objects.""" spec = ProcessSpec("A process spec", uids={'id': str(uuid4()), 'auto': str(uuid4())}) run1 = ProcessRun("A process run", spec=spec, uids={'id': str(uuid4()), 'auto': str(uuid4())}) run2 = ProcessRun("Another process run", spec=spec, uids={'id': str(uuid4())}) process_dict = {spec: [run1, run2]} subbed = substitute_links(process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(spec, scope='auto') assert LinkByUID.from_entity(run1, scope='auto') in value assert LinkByUID.from_entity(run2) in value reverse_process_dict = {run2: spec} subbed = substitute_links(reverse_process_dict, scope='auto') for key, value in subbed.items(): assert key == LinkByUID.from_entity(run2) assert value == LinkByUID.from_entity(spec, scope='auto')
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf)) copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_make_index(): """Test functionality of make_index method.""" ps1 = ProcessSpec(name="hello", uids={"test_scope": "test_value"}) pr1 = ProcessRun( name="world", spec=LinkByUID(scope="test_scope", id="test_value"), uids={"test_scope": "another_test_value", "other_test": "also_valid" }, ) ms1 = MaterialSpec( name="material", process=LinkByUID(scope="test_scope", id="test_value"), uids={"second_scope": "this_is_an_id"}, ) mr1 = MaterialRun( name="material_run", spec=LinkByUID(scope="second_scope", id="this_is_an_id"), process=LinkByUID(scope="test_scope", id="another_test_value"), ) pr2 = ProcessRun( name="goodbye", spec=LinkByUID.from_entity(ps1), uids={"test_scope": "the_other_value"}, ) mr2 = MaterialRun( name="cruel", spec=LinkByUID.from_entity(ms1), process=LinkByUID.from_entity(pr2), ) gems = [ps1, pr1, ms1, mr1, pr2, mr2] gem_index = make_index(gems) for gem in gems: for scope in gem.uids: assert (scope, gem.uids[scope]) in gem_index assert gem_index[(scope, gem.uids[scope])] == gem # Make sure substitute_objects can consume the index subbed = substitute_objects(mr1, gem_index) assert subbed.spec.uids == ms1.uids
def test_template_access(): """A measurement run's template should be equal to its spec's template.""" template = MeasurementTemplate("measurement template", uids={'id': str(uuid4())}) spec = MeasurementSpec("A spec", uids={'id': str(uuid4())}, template=template) meas = MeasurementRun("A run", uids={'id': str(uuid4())}, spec=spec) assert meas.template == template meas.spec = LinkByUID.from_entity(spec) assert meas.template is None
def test_link_by_uid(): """Test that linking works.""" root = MaterialRun(name='root', process=ProcessRun(name='root proc')) leaf = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) IngredientRun(process=root.process, material=leaf) IngredientRun(process=root.process, material=LinkByUID.from_entity(leaf, scope='id')) # Paranoid assertions about equality's symmetry since it's implemented in 2 places assert root.process.ingredients[0].material == root.process.ingredients[ 1].material assert root.process.ingredients[0].material.__eq__( root.process.ingredients[1].material) assert root.process.ingredients[1].material.__eq__( root.process.ingredients[0].material) # Verify hash collision on equal LinkByUIDs assert LinkByUID.from_entity(leaf) in {LinkByUID.from_entity(leaf)} copy = loads(dumps(root)) assert copy.process.ingredients[0].material == copy.process.ingredients[ 1].material
def test_from_entity(): """Test permutations of LinkByUID.from_entity arguments.""" run = MaterialRun(name='leaf', process=ProcessRun(name='leaf proc')) assert LinkByUID.from_entity(run).scope == 'auto' assert LinkByUID.from_entity(run, scope='missing').scope == 'auto' assert len(run.uids) == 1 run.uids['foo'] = 'bar' link1 = LinkByUID.from_entity(run, scope='foo') assert (link1.scope, link1.id) == ('foo', 'bar') with pytest.deprecated_call(): assert LinkByUID.from_entity(run, 'foo').scope == 'foo' with pytest.deprecated_call(): assert LinkByUID.from_entity(run, name='foo').scope == 'foo' with pytest.raises(ValueError): LinkByUID.from_entity(run, name='scope1', scope='scope2')
def _async_gemd_batch_delete( id_list: List[Union[LinkByUID, UUID, str, BaseEntity]], project_id: UUID, session: Session, dataset_id: Optional[UUID], timeout: float = 2 * 60, polling_delay: float = 1.0) -> List[Tuple[LinkByUID, ApiError]]: """ Shared implementation of Async GEMD Batch deletion. See documentation for _gemd_batch_delete. The only difference is that this version polls for an asynchronous result and can tolerate a very long runtime that the synchronous version cannot. Because this version can tolerate a long runtime, this versions allows for the removal of attribute templates. Parameters ---------- id_list: List[Union[LinkByUID, UUID, str, BaseEntity]] A list of the IDs of data objects to be removed. They can be passed as a LinkByUID tuple, a UUID, a string, or the object itself. A UUID or string is assumed to be a Citrine ID, whereas a LinkByUID or BaseEntity can also be used to provide an external ID. project_id: UUID The Project ID to use in the delete request. session: Session The Citrine session. dataset_id: Optional[UUID] = None An optional dataset ID, which if provided will mandate that all GEMD objects must be within the given dataset. timeout: float Amount of time to wait on the job (in seconds) before giving up. Defaults to 2 minutes. Note that this number has no effect on the underlying job itself, which can also time out server-side. polling_delay: float How long to delay between each polling retry attempt. Returns ------- List[Tuple[LinkByUID, ApiError]] A list of (LinkByUID, api_error) for each failure to delete an object. Note that this method doesn't raise an exception if an object fails to be deleted. """ scoped_uids = [] for uid in id_list: # And now normalize to id/scope pairs if isinstance(uid, BaseEntity): link_by_uid = LinkByUID.from_entity(uid, CITRINE_SCOPE) scoped_uids.append({ 'scope': link_by_uid.scope, 'id': link_by_uid.id }) elif isinstance(uid, LinkByUID): scoped_uids.append({'scope': uid.scope, 'id': uid.id}) elif isinstance(uid, UUID): scoped_uids.append({'scope': 'id', 'id': uid}) elif isinstance(uid, str): try: scoped_uids.append({'scope': 'id', 'id': UUID(uid)}) except ValueError: raise TypeError("{} does not look like a UUID".format(uid)) else: raise TypeError( "id_list must contain only LinkByUIDs, UUIDs, strings, or BaseEntities" ) body = {'ids': scoped_uids} if dataset_id is not None: body.update({'dataset_id': str(dataset_id)}) path = '/projects/{project_id}/gemd/async-batch-delete'.format( **{"project_id": project_id}) response = session.post_resource(path, body) job_id = response["job_id"] return _poll_for_async_batch_delete_result(project_id, session, job_id, timeout, polling_delay)