def test_find_or_create_container_copy(self, mocker, found): find_mock = mocker.patch( "container_export.ContainerExporter.find_container_copy") find_mock.return_value = found create_mock = mocker.patch( "container_export.ContainerExporter.create_container_copy") create_mock.return_value = flywheel.Subject(label="test2") container, created = ContainerExporter.find_or_create_container_copy( flywheel.Subject(label="test"), "test") assert created == (found is None) assert (container == flywheel.Subject(label="test") if not found is None else flywheel.Subject(label="test2"))
class TestGetDestination: @pytest.mark.parametrize( "parent,raising", [ (flywheel.Subject(label="test"), does_not_raise()), (flywheel.Session(label="test"), does_not_raise()), (flywheel.Group(label="test"), pytest.raises(ValueError)), (flywheel.Project(label="test"), pytest.raises(ValueError)), (flywheel.Acquisition(label="test"), pytest.raises(ValueError)), ], ) def test_container(self, sdk_mock, parent, raising): container = flywheel.models.analysis_output.AnalysisOutput( parent=parent, id="test" ) sdk_mock.get_analysis.return_value = container sdk_mock.get.return_value = parent with raising: dest = get_destination(sdk_mock, "test") sdk_mock.get_analysis.assert_called_once_with("test") # assert dest.__class__ == parent.__class__ assert isinstance(dest, parent.__class__) def test_analysis_does_not_exist(self, sdk_mock): container = flywheel.models.analysis_output.AnalysisOutput( parent=flywheel.Project(), id="test" ) sdk_mock.get.side_effect = flywheel.rest.ApiException(status=404) sdk_mock.get_analysis.return_value = container with pytest.raises(flywheel.rest.ApiException): dest = get_destination(sdk_mock, "test") assert isinstance(dest, flywheel.Project)
class TestNeedsExport: @pytest.mark.parametrize( "dest, tags, force, result", [ (flywheel.Subject(id="test"), [], True, True), (flywheel.Subject(id="test"), [], False, True), (flywheel.Subject(id="test"), ["EXPORTED"], True, True), (flywheel.Subject(id="test"), ["EXPORTED"], False, False), (flywheel.Session(id="test"), [], True, True), (flywheel.Session(id="test"), [], False, True), (flywheel.Session(id="test"), ["EXPORTED"], True, True), (flywheel.Session(id="test"), ["EXPORTED"], False, False), ], ) def test_container_exists(self, dest, tags, force, result): dest.tags = tags out = container_needs_export(dest, {"force_export": force}) assert out == result
def test_validate_calls(self, mocker, gear_context, config, call_num, caplog): caplog.set_level(logging.INFO) mock_proj = ( flywheel.Project( label="test", parents=flywheel.models.container_parents.ContainerParents( group="test" ), ), ) gear_context.config = config get_proj_mock = mocker.patch("validate.get_project") get_proj_mock.return_value = mock_proj get_dest_mock = mocker.patch("validate.get_destination") get_dest_mock.return_value = flywheel.Subject(label="test") check_exported_mock = mocker.patch("validate.container_needs_export") check_exported_mock.return_value = True check_gear_rules_mock = mocker.patch("validate.validate_gear_rules") check_gear_rules_mock.return_value = True export, archive, dest = validate_context(gear_context) assert get_proj_mock.call_count == call_num get_dest_mock.assert_called_once_with(gear_context.client, "test") check_exported_mock.assert_called_once_with( flywheel.Subject(label="test"), config ) msgs = [rec.message for rec in caplog.records] if "check_gear_rules" in config: check_gear_rules_mock.assert_called_once_with( gear_context.client, mock_proj ) assert "No enabled rules were found. Moving on..." in msgs else: check_gear_rules_mock.assert_not_called() assert "No enabled rules were found. Moving on..." not in msgs
def test_subject_errors(self): fw = self.fw # Try to create subject without project id try: subject = flywheel.Subject(code=self.rand_string()) subject_id = fw.add_subject(subject) self.fail('Expected ApiException creating invalid subject!') except flywheel.ApiException as e: self.assertEqual(e.status, 422) # Try to get a subject that doesn't exist try: fw.get_subject('DOES_NOT_EXIST') self.fail('Expected ApiException retrieving invalid subject!') except flywheel.ApiException as e: self.assertEqual(e.status, 404)
def test_subject_analysis(self): fw = self.fw subject = flywheel.Subject(project=self.project_id, code=self.rand_string()) # Add subject_id = fw.add_subject(subject) self.assertNotEmpty(subject_id) poem = 'When a vast image out of Spiritus Mundi' fw.upload_file_to_subject(subject_id, flywheel.FileSpec('yeats.txt', poem)) file_ref = flywheel.FileReference( id=subject_id, type='subject', name='yeats.txt' ) analysis = flywheel.AnalysisInput(label=self.rand_string(), description=self.rand_string(), inputs=[file_ref]) # Add analysis_id = fw.add_subject_analysis(subject_id, analysis) self.assertNotEmpty(analysis_id) # Get the list of analyses in the subject analyses = fw.get_subject_analyses(subject_id) self.assertEqual(len(analyses), 1) r_analysis = analyses[0] self.assertEqual(r_analysis.id, analysis_id) self.assertEmpty(r_analysis.job) self.assertTimestampBeforeNow(r_analysis.created) self.assertGreaterEqual(r_analysis.modified, r_analysis.created) self.assertEqual(len(r_analysis.inputs), 1) self.assertEqual(r_analysis.inputs[0].name, 'yeats.txt')
def test_subjects(self): fw = self.fw subject_code = self.rand_string_lower() subject = flywheel.Subject( project=self.project_id, firstname=self.rand_string(), lastname=self.rand_string(), code=subject_code, sex='other', info={'some-subject-key': 37} ) # Add subject_id = fw.add_subject(subject) self.assertNotEmpty(subject_id) # Get r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.id, subject_id) self.assertEqual(r_subject.code, subject_code) self.assertIn('some-subject-key', r_subject.info) self.assertEqual(r_subject.info['some-subject-key'], 37) self.assertTimestampBeforeNow(r_subject.created) self.assertGreaterEqual(r_subject.modified, r_subject.created) # Generic Get is equivalent self.assertEqual(fw.get(subject_id).to_dict(), r_subject.to_dict()) # Get All subjects = fw.get_all_subjects() self.assertNotEmpty(subjects) self.sanitize_for_collection(r_subject) self.assertIn(r_subject, subjects) # # Get from parent # subjects = fw.get_project_subjects(self.project_id) # self.assertIn(r_subject, subjects) # Modify new_sex = 'male' r_subject.update(sex=new_sex) changed_subject = fw.get_subject(subject_id) self.assertEqual(changed_subject.sex, new_sex) self.assertEqual(changed_subject.created, r_subject.created) self.assertGreater(changed_subject.modified, r_subject.modified) # Notes, Tags message = 'This is a note' r_subject.add_note(message) tag = 'example-tag' r_subject.add_tag(tag) # Replace Info fw.replace_subject_info(subject_id, { 'foo': 3, 'bar': 'qaz' }) # Set Info fw.set_subject_info(subject_id, { 'foo': 42, 'hello': 'world' }) # Check r_subject = fw.get_subject(subject_id) self.assertEqual(len(r_subject.notes), 1) self.assertEqual(r_subject.notes[0].text, message) self.assertEqual(len(r_subject.tags), 1) self.assertEqual(r_subject.tags[0], tag) self.assertEqual(r_subject.info['foo'], 42) self.assertEqual(r_subject.info['bar'], 'qaz') self.assertEqual(r_subject.info['hello'], 'world') # Delete info fields fw.delete_subject_info_fields(subject_id, ['foo', 'bar']) r_subject = fw.get_subject(subject_id) self.assertNotIn('foo', r_subject.info) self.assertNotIn('bar', r_subject.info) self.assertEqual(r_subject.info['hello'], 'world') # Add session r_session = r_subject.add_session(label='Session 1') self.assertEqual(r_session.project, self.project_id) self.assertEqual(r_session.subject.id, subject_id) # Delete fw.delete_subject(subject_id) subjects = fw.get_all_subjects() self.sanitize_for_collection(r_subject) self.assertNotIn(r_subject, subjects)
def test_subject_files(self): fw = self.fw subject = flywheel.Subject(code=self.rand_string(), project=self.project_id) subject_id = fw.add_subject(subject) # Upload a file poem = 'The best lack all conviction, while the worst' fw.upload_file_to_subject(subject_id, flywheel.FileSpec('yeats.txt', poem)) # Check that the file was added to the subject r_subject = fw.get_subject(subject_id) self.assertEqual(len(r_subject.files), 1) self.assertEqual(r_subject.files[0].name, 'yeats.txt') self.assertEqual(r_subject.files[0].size, 45) self.assertEqual(r_subject.files[0].mimetype, 'text/plain') # Download the file and check content self.assertDownloadFileTextEquals(fw.download_file_from_subject_as_data, subject_id, 'yeats.txt', poem) # Test unauthorized download with ticket for the file self.assertDownloadFileTextEqualsWithTicket(fw.get_subject_download_url, subject_id, 'yeats.txt', poem) # Test file attributes self.assertEqual(r_subject.files[0].modality, None) self.assertEmpty(r_subject.files[0].classification) self.assertEqual(r_subject.files[0].type, 'text') resp = r_subject.files[0].update(type='type', modality='modality') # Check that no jobs were triggered, and attrs were modified self.assertEqual(resp.jobs_spawned, 0) r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.files[0].modality, "modality") self.assertEmpty(r_subject.files[0].classification) self.assertEqual(r_subject.files[0].type, 'type') # Test classifications resp = fw.replace_subject_file_classification(subject_id, 'yeats.txt', { 'Custom': ['measurement1', 'measurement2'], }) self.assertEqual(resp.modified, 1) self.assertEqual(resp.jobs_spawned, 0) r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.files[0].classification, { 'Custom': ['measurement1', 'measurement2'] }); resp = fw.modify_subject_file_classification(subject_id, 'yeats.txt', { 'add': { 'Custom': ['HelloWorld'], }, 'delete': { 'Custom': ['measurement2'] } }) self.assertEqual(resp.modified, 1) self.assertEqual(resp.jobs_spawned, 0) r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.files[0].classification, { 'Custom': ['measurement1', 'HelloWorld'], }); # Test file info self.assertEmpty(r_subject.files[0].info) fw.replace_subject_file_info(subject_id, 'yeats.txt', { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }) fw.set_subject_file_info(subject_id, 'yeats.txt', { 'c': 5 }) r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.files[0].info['a'], 1) self.assertEqual(r_subject.files[0].info['b'], 2) self.assertEqual(r_subject.files[0].info['c'], 5) self.assertEqual(r_subject.files[0].info['d'], 4) fw.delete_subject_file_info_fields(subject_id, 'yeats.txt', ['c', 'd']) r_subject = fw.get_subject(subject_id) self.assertEqual(r_subject.files[0].info['a'], 1) self.assertEqual(r_subject.files[0].info['b'], 2) self.assertNotIn('c', r_subject.files[0].info) self.assertNotIn('d', r_subject.files[0].info) fw.replace_subject_file_info(subject_id, 'yeats.txt', {}) r_subject = fw.get_subject(subject_id) self.assertEmpty(r_subject.files[0].info) # Delete file fw.delete_subject_file(subject_id, 'yeats.txt') r_subject = fw.get_subject(subject_id) self.assertEmpty(r_subject.files) # Delete subject fw.delete_subject(subject_id)
def test_sessions(self): fw = self.fw session_name = self.rand_string() session = flywheel.Session(label=session_name, project=self.project_id, info={'some-key': 37}, subject=flywheel.Subject( code=self.rand_string_lower(), firstname=self.rand_string(), lastname=self.rand_string(), sex='other', age=util.years_to_seconds(56), info={'some-subject-key': 37})) # Add session_id = fw.add_session(session) self.assertNotEmpty(session_id) # Get r_session = fw.get_session(session_id) self.assertEqual(r_session.id, session_id) self.assertEqual(r_session.label, session_name) self.assertIn('some-key', r_session.info) self.assertEqual(r_session.info['some-key'], 37) self.assertTimestampBeforeNow(r_session.created) self.assertGreaterEqual(r_session.modified, r_session.created) self.assertIsNotNone(r_session.subject) self.assertEqual(r_session.subject.firstname, session.subject.firstname) self.assertEqual(r_session.age_years, 56) # Generic Get is equivalent self.assertEqual(fw.get(session_id).to_dict(), r_session.to_dict()) # Get All sessions = fw.get_all_sessions() self.assertNotEmpty(sessions) self.sanitize_for_collection(r_session) self.assertIn(r_session, sessions) # Get from parent sessions = fw.get_project_sessions(self.project_id) self.assertIn(r_session, sessions) # Modify new_name = self.rand_string() session_mod = flywheel.Session(label=new_name) fw.modify_session(session_id, session_mod) changed_session = fw.get_session(session_id) self.assertEqual(changed_session.label, new_name) self.assertEqual(changed_session.created, r_session.created) self.assertGreater(changed_session.modified, r_session.modified) # Notes, Tags message = 'This is a note' fw.add_session_note(session_id, message) tag = 'example-tag' fw.add_session_tag(session_id, tag) # Replace Info fw.replace_session_info(session_id, {'foo': 3, 'bar': 'qaz'}) # Set Info fw.set_session_info(session_id, {'foo': 42, 'hello': 'world'}) # Check r_session = fw.get_session(session_id) self.assertEqual(len(r_session.notes), 1) self.assertEqual(r_session.notes[0].text, message) self.assertEqual(len(r_session.tags), 1) self.assertEqual(r_session.tags[0], tag) self.assertEqual(r_session.info['foo'], 42) self.assertEqual(r_session.info['bar'], 'qaz') self.assertEqual(r_session.info['hello'], 'world') # Delete info fields fw.delete_session_info_fields(session_id, ['foo', 'bar']) r_session = fw.get_session(session_id) self.assertNotIn('foo', r_session.info) self.assertNotIn('bar', r_session.info) self.assertEqual(r_session.info['hello'], 'world') # Delete fw.delete_session(session_id) sessions = fw.get_all_sessions() self.sanitize_for_collection(r_session) self.assertNotIn(r_session, sessions)
class TestContainerExporter: @pytest.mark.parametrize( "origin,raises", [ (flywheel.Session(label="origin"), does_not_raise()), ("origin", pytest.raises(AttributeError)), ], ) def test_init(self, mocker, origin, raises): gear_context_mock = MagicMock( spec=dir(flywheel_gear_toolkit.GearToolkitContext)) hierarchy_patch = mocker.patch( "container_export.ContainerExporter.get_hierarchy") log_patch = mocker.patch("container_export.ExportLog") exporter = None with raises: exporter = ContainerExporter("export", "archive", origin, gear_context_mock) hierarchy_patch.assert_called_once_with(origin) # Validate attributes if exporter is called if hasattr(origin, "container_type"): log_patch.assert_called_once_with("export", "archive") for attr in [ "status", "_log", ]: assert getattr(exporter, attr) is None assert exporter.gear_context == gear_context_mock assert exporter.origin_container == origin assert exporter.container_type == origin.container_type @pytest.mark.parametrize( "origin", [flywheel.Subject(code="origin"), flywheel.Session(label="origin")]) def test_from_gear_context(self, mocker, origin): gear_context_mock = MagicMock( spec=dir(flywheel_gear_toolkit.GearToolkitContext)) log_patch = mocker.patch("container_export.ExportLog") hierarchy_patch = mocker.patch( "container_export.ContainerExporter.get_hierarchy") validate_patch = mocker.patch("container_export.validate_context") export_proj = flywheel.Project(label="export") archive_proj = (flywheel.Project(label="archive"), ) validate_patch.return_value = [ export_proj, archive_proj, origin, ] exporter = ContainerExporter.from_gear_context(gear_context_mock) assert exporter.origin_container == origin log_patch.assert_called_once_with(export_proj, archive_proj) hierarchy_patch.assert_called_once_with(origin) def test_log(self, mocker, container_export): export, mocks = container_export("test", "test", flywheel.Session(), mock=True) log_mock = mocker.patch("container_export.logging.getLogger") log = export.log log_mock.assert_called_once_with("GRP-9 Session None Export") @pytest.mark.parametrize( "container,exp", [ (flywheel.Session(), "test-None_export_log.csv"), (flywheel.Subject(), "test_export_log.csv"), ], ) def test_csv_path(self, mocker, container_export, container, exp): export, mocks = container_export("test", "test", container, mock=True) mocks["hierarchy"].return_value.subject.label = "test" mocks["context"].output_dir = "/tmp/gear" path = export.csv_path assert path == f"/tmp/gear/{exp}" def test_get_hierarchy(self, mocker, container_export): hierarchy_mock = mocker.patch( "container_export.ContainerHierarchy.from_container") log_mock = mocker.patch("container_export.ExportLog") export, mocks = container_export("test", "test", flywheel.Session()) hierarchy_mock.assert_called_once_with(mocks["context"].client, flywheel.Session()) @pytest.mark.parametrize("info", [{"test": "test"}, {"test": None}, {}]) @pytest.mark.parametrize("ctype,other", [("Session", { "age": "10" }), ("Subject", { "sex": "F" })]) def test_get_create_container_kwargs(self, mocker, c, info, ctype, other): container = c(ctype, id="test", info=info, **other) out = ContainerExporter.get_create_container_kwargs(container) assert all([key in out for key in other.keys()]) assert out.get("info").get("export").get("origin_id") == hash_value( "test") info.update({"export": {"origin_id": hash_value("test")}}) assert info == out.get("info") @pytest.mark.parametrize( "container,label", [ ( flywheel.Session(id="test"), (f"info.export.origin_id={hash_value('test')}", ), ), ( flywheel.Subject(label="test", code="test"), ("label=test", "code=test"), ), ( flywheel.Subject(label="5", code="5"), ('label="5"', 'code="5"'), ), ], ) def test_get_container_find_queries(self, container, label): queries = ContainerExporter.get_container_find_queries(container) assert queries == label @pytest.mark.parametrize("same", [True, False]) @pytest.mark.parametrize( "origin,export,parent,par_type", [ ( flywheel.Session(id="test"), flywheel.Session( id="test2", info={"export": { "origin_id": hash_value("test") }}), MagicMock(spec=dir(flywheel.Subject).extend("sessions")), "subject", ), ( flywheel.Subject(label="test"), flywheel.Subject(label="test"), MagicMock(spec=dir(flywheel.Project).extend("subjects")), "project", ), ], ) def test_find_container_copy(self, origin, export, parent, mocker, par_type, same): parent.container_type = par_type parent.id = "test_parent" export.parents = flywheel.ContainerParents(**{par_type: parent.id}) origin.parents = flywheel.ContainerParents( **{par_type: parent.id if same else "test_parent2"}) finder_mock = getattr(parent, f"{export.container_type}s").find_first finder_mock.return_value = export out = ContainerExporter.find_container_copy(origin, parent) if not same: if par_type == "project": finder_mock.assert_called_once_with("label=test") else: finder_mock.assert_called_once_with( f"info.export.origin_id={hash_value('test')}") else: assert out == origin @pytest.mark.parametrize( "origin,parent", [ (flywheel.Session(id="test"), MagicMock(spec=dir(flywheel.Subject))), ( flywheel.Session(id="test", tags=["test", "one"]), MagicMock(spec=dir(flywheel.Subject)), ), ], ) def test_create_container_copy(self, origin, parent): add_mock = getattr(parent, f"add_{origin.container_type}") add_mock.return_value = origin out = ContainerExporter.create_container_copy(origin, parent) assert out.label == origin.label assert out.tags == origin.tags @pytest.mark.parametrize("found", [None, flywheel.Subject(label="test")]) def test_find_or_create_container_copy(self, mocker, found): find_mock = mocker.patch( "container_export.ContainerExporter.find_container_copy") find_mock.return_value = found create_mock = mocker.patch( "container_export.ContainerExporter.create_container_copy") create_mock.return_value = flywheel.Subject(label="test2") container, created = ContainerExporter.find_or_create_container_copy( flywheel.Subject(label="test"), "test") assert created == (found is None) assert (container == flywheel.Subject(label="test") if not found is None else flywheel.Subject(label="test2")) @pytest.mark.parametrize("base", [flywheel.Subject, flywheel.Session]) def test_export_container_files(self, sdk_mock, mocker, base): exporter_mock = mocker.patch( "container_export.FileExporter.from_client") origin = base(files=[]) side_effect = [] for i in range(10): origin.files.append(flywheel.FileEntry(name=str(i))) side_effect.append((str(i) if i % 2 == 0 else None, True if i % 3 == 0 else False)) exporter_mock.return_value.find_or_create_file_copy.side_effect = side_effect found, created, failed = ContainerExporter.export_container_files( sdk_mock, origin, "other", None) assert failed == ["1", "3", "5", "7", "9"] assert created == ["0", "6"] assert found == ["2", "4", "8"] @pytest.mark.parametrize( "container", [ flywheel.Subject(label="test"), flywheel.Session(label="test", subject=flywheel.Subject(label="test")), ], ) def test_get_subject_export_params(self, mocker, container_export, container): if container.container_type == "subject": cont_mock = mocker.patch.object(container, "reload") cont_mock.return_value = "mocked" else: cont_mock = mocker.patch.object(container.subject, "reload") cont_mock.return_value = "mocked" export, mocks = container_export("test", "test", container, mock=True) orig, proj, att, hier = export.get_subject_export_params() assert orig == "mocked" assert proj == "test" if container.container_type == "subject": assert att == None else: assert att == False assert mocks["hierarchy"].call_count == 1 @pytest.mark.parametrize( "origin,ctype", [ (MagicMock(spec=dir(flywheel.Session)), "session"), (MagicMock(spec=(dir(flywheel.Subject) + ["sessions"])), "subject"), ], ) def test_get_origin_sessions(self, container_export, origin, ctype): origin.container_type = ctype if ctype == "subject": origin.sessions.iter.return_value = ["1", "2", "3"] else: origin.reload.return_value = origin container_ex, mocks = container_export("test", "test", origin, mock=True) sess = container_ex.get_origin_sessions() if ctype == "subject": assert sess == ["1", "2", "3"] else: assert sess == [origin]
def test_container_hierarchy(): hierarchy_dict = { "group": flywheel.Group(id="test_group", label="Test Group"), "project": flywheel.Project(label="test_project"), "subject": flywheel.Subject(label="test_subject", sex="other"), "session": flywheel.Session( age=31000000, label="test_session", weight=50, ), } # test from_dict test_hierarchy = ContainerHierarchy.from_dict(hierarchy_dict) # test deepcopy assert deepcopy(test_hierarchy) != test_hierarchy # test path assert test_hierarchy.path == "test_group/test_project/test_subject/test_session" # test parent assert test_hierarchy.parent.label == "test_subject" # test from_container mock_client = MagicMock(spec=dir(flywheel.Client)) parent_dict = dict() for item in ("group", "project", "subject"): value = hierarchy_dict.copy().get(item) parent_dict[item] = item setattr(mock_client, f"get_{item}", lambda x: value) session = flywheel.Session(age=31000000, label="test_session", weight=50) session.parents = parent_dict assert (ContainerHierarchy.from_container( mock_client, session).container_type == "session") # test _get_container assert test_hierarchy._get_container(None, None, None) is None with pytest.raises(ValueError) as exc: test_hierarchy._get_container(None, "garbage", "garbage_id") assert str(exc) == "Cannot get a container of type garbage" mock_client = MagicMock(spec=dir(flywheel.Client)) mock_client.get_session = lambda x: x assert (test_hierarchy._get_container(mock_client, "session", "session_id") == "session_id") # test container_type assert test_hierarchy.container_type == "session" # test dicom_map exp_map = { "PatientWeight": 50, "PatientAge": "011M", "ClinicalTrialTimePointDescription": "test_session", "PatientSex": "O", "PatientID": "test_subject", } assert exp_map == test_hierarchy.dicom_map # test get assert test_hierarchy.get("container_type") == "session" # test get_patient_sex_from_subject assert test_hierarchy.get_patientsex_from_subject(flywheel.Subject()) == "" # test get_patientage_from_session assert test_hierarchy.get_patientage_from_session( flywheel.Session()) is None # test get_child_hierarchy test_acquisition = flywheel.Acquisition(label="test_acquisition") acq_hierarchy = test_hierarchy.get_child_hierarchy(test_acquisition) assert acq_hierarchy.dicom_map[ "SeriesDescription"] == test_acquisition.label # test get_parent_hierarchy parent_hierarchy = test_hierarchy.get_parent_hierarchy() assert parent_hierarchy.container_type == "subject"