diff --git a/pysyncrosim/helper.py b/pysyncrosim/helper.py index 1522ea2..8f673ab 100644 --- a/pysyncrosim/helper.py +++ b/pysyncrosim/helper.py @@ -236,7 +236,7 @@ def _delete_project(library, name=None, pid=None, session=None, # Delete Project using console if pid is None: - pid = p["ID"].values[0] + pid = p["ProjectId"].values[0] args = ["--delete", "--project", "--lib=\"%s\"" % library.location, "--pid=%d" % pid, "--force"] session._Session__call_console(args) @@ -268,7 +268,7 @@ def _delete_scenario(library, project, name=None, sid=None, session=None, # Delete Scenario using console if sid is None: - sid = s["Scenario ID"].values[0] + sid = s["ScenarioId"].values[0] args = ["--delete", "--scenario", "--lib=\"%s\"" % library.location, "--sid=%d" % sid, "--force"] session._Session__call_console(args) @@ -277,3 +277,55 @@ def _delete_scenario(library, project, name=None, sid=None, session=None, library._Library__scenarios = None library._Library__init_scenarios() +def _delete_folder(library, fid, session=None, force=False): + + if session is None: + session = ps.Session() + + if force is False: + answer = input(f"Are you sure you want to delete folder {fid} (Y/N)?") + else: + answer = "Y" + + if answer == "Y": + + # Retrieve Folder DataFrame + args = [f"--lib={library.location}", "--list", "--folders"] + folder_data = session._Session__call_console(args, decode=True, csv=True) + folder_df = pd.read_csv(io.StringIO(folder_data)) + + if fid not in folder_df["Id"].values: + raise ValueError(f"Folder ID {fid} does not exist") + + # Delete Folder using Console + args = ["--delete", "--folder", f"--lib={library.location}", f"--fid={fid}", "--force"] + + session._Session__call_console(args) + +def _delete_data(library, datasheet, pid=None, sid=None, ids=None, + session=None, force=False): + + if session is None: + session = ps.Session() + + if force is False: + if ids is not None: + answer = input(f"Are you sure you want to delete rows {ids} from {datasheet} (Y/N)?") + else: + answer = input(f"Are you sure you want to delete all data from {datasheet} (Y/N)?") + else: + answer = "Y" + + if answer == "Y": + + # Delete Data using Console + args = ["--delete", "--data", f"--lib={library.location}", f"--sheet={datasheet}", "--force"] + + if pid is not None: + args += [f"--pid={pid}"] + if sid is not None: + args += [f"--sid={sid}"] + if ids is not None: + args += [f'--ids="{ids}"'] + + session._Session__call_console(args) diff --git a/pysyncrosim/library.py b/pysyncrosim/library.py index c046b24..4f6e927 100644 --- a/pysyncrosim/library.py +++ b/pysyncrosim/library.py @@ -613,7 +613,8 @@ def datasheets(self, name=None, summary=True, optional=False, empty=False, return ds - def delete(self, project=None, scenario=None, force=False, remove_backup=False, remove_publish=False, remove_custom_folders=False): + def delete(self, project=None, scenario=None, folder=None, + datasheet=None, ids=None, force=False, remove_backup=False, remove_publish=False, remove_custom_folders=False): """ Deletes a SyncroSim class instance. @@ -623,17 +624,29 @@ def delete(self, project=None, scenario=None, force=False, remove_backup=False, If called from a Library class instance, specify the Project to delete. The default is None. scenario : Scenario, String, or Int, optional - If called from a Scenario class instance, specify the Scenario to + If called from a Project class instance, specify the Scenario to delete. The default is None. + folder : Folder, or Int, optional + If called from a Library class instance, specify the folder to delete. The default is None. + datasheet : String, optional + Name of the datasheet to delete data from. The default is None. + ids : String, optional + The primary key IDs for the rows to delete from the datasheet. Only used when a datasheet name is provided. The default is None. force : Logical, optional If set to True, does not ask user before deleting SyncroSim class instance. The default is False. remove_backup : Logical, optional - If True, will remove the backup folder when deleting a Library. Default is False. + If True, will remove the backup folder when deleting a Library. + Default is False. remove_publish : Logical, optional - If True, will remove the publish folder when deleting a Library. Default is False. + If True, will remove the publish folder when deleting a Library. + Default is False. remove_custom_folders : Logical, optional - If True and custom folders have been configured for a Library, then will remove the custom publish and/or backup folders when deleting a Library. Note that the remove_publish and remove_backup arguments must also be set to True to remove the respective custom folders. Default is False. + If True and custom folders have been configured for a Library, then + will remove the custom publish and/or backup folders when deleting + a Library. Note that the remove_publish and remove_backup arguments + must also be set to True to remove the respective custom folders. + Default is False. Returns ------- @@ -645,16 +658,11 @@ def delete(self, project=None, scenario=None, force=False, remove_backup=False, # Also, should have method to delete list of Projects or Scenarios? # type checks - if project is not None and not isinstance(project, ps.Project): - if not isinstance(project, int) and not isinstance( - project, str) and not isinstance(project, np.int64): - raise TypeError( - "project must be a Project instance, Integer, or String") - if scenario is not None and not isinstance(scenario, ps.Scenario): - if not isinstance(scenario, int) and not isinstance( - scenario, str) and not isinstance(scenario, np.int64): - raise TypeError( - "scenario must be a Scenario instance, Integer, or String") + if folder is not None and not isinstance(folder, ps.Folder): + if not isinstance(folder, int) and not isinstance(folder, np.int64): + raise TypeError("folder must be a Folder instance or Integer") + if datasheet is not None and not isinstance(datasheet, str): + raise TypeError("datasheet must be a String") if not isinstance(force, bool): raise TypeError("force must be a Logical") @@ -665,45 +673,79 @@ def delete(self, project=None, scenario=None, force=False, remove_backup=False, if not isinstance(remove_custom_folders, bool): raise TypeError("remove_custom_folders must be a Logical") - if project is None and scenario is None: - + # delete datasheet + if datasheet is not None: + helper._delete_data(library=self, datasheet=datasheet, ids=ids, + session=self.session, force=force) + + # delete library + if project is None and scenario is None and folder is None and\ + datasheet is None: helper._delete_library(name = self.location, session=self.session, - force=force, remove_backup=remove_backup, remove_publish=remove_publish, remove_custom_folders=remove_custom_folders) - + force=force, remove_backup=remove_backup, + remove_publish=remove_publish, + remove_custom_folders=remove_custom_folders) + + # delete project scope elif project is not None and scenario is None: # turn project into project class instance if str or int - if type(project) is int: - p = self.projects(pid = project) - if type(project) is str: + if type(project) is int or isinstance(project, np.int64): + if project in self.__projects["ProjectId"].values: + p = self.projects(pid = project) + else: + raise ValueError(f"project {project} does not exist") + elif type(project) is str: if project in self.__projects["Name"].values: p = self.projects(name = project) else: - raise ValueError(f'project {project} does not exist') - if isinstance(project, ps.Project): + raise ValueError(f"project {project} does not exist") + elif isinstance(project, ps.Project): p = project + else: + raise TypeError(f"project must be a Project instance, " + f"Integer, or String") helper._delete_project(library=self, name=p.name, - pid=p.pid, session=self.session, - force=force) - + session=self.session, force=force) + + # delete scenario elif scenario is not None: # turn scenario into scenario class instance if str or int - if type(scenario) is int: - s = self.scenarios(sid = scenario, project = project) - if type(scenario) is str: + if type(scenario) is int or isinstance(scenario, np.int64): + if scenario in self.__scenarios["ScenarioId"].values: + s = self.scenarios(sid = scenario, project = project) + else: + raise ValueError(f"scenario {scenario} does not exist") + elif type(scenario) is str: if scenario in self.__scenarios["Name"].values: s = self.scenarios(name = scenario, project = project) else: - raise ValueError(f'scenario {scenario} does not exist') - if isinstance(scenario, ps.Scenario): + raise ValueError(f"scenario {scenario} does not exist") + elif isinstance(scenario, ps.Scenario): s = scenario + else: + raise TypeError(f"scenario must be a Scenario instance, " + f"Integer, or String") - helper._delete_scenario(library=self, project=s.project, - name=s.name, sid=s.sid, - session=self.session, + helper._delete_scenario(library=self, project=s.project, + name=s.name, session=self.session, force=force) + + # delete folder + elif folder is not None: + + # turn folder into folder ID if int + if type(folder) is int or isinstance(folder, np.int64): + fid = folder + elif isinstance(folder, ps.Folder): + fid = folder.folder_id + else: + raise ValueError(f"folder {folder} does not exist") + + helper._delete_folder(library=self, fid=fid, session=self.session, + force=force) def save_datasheet(self, name, data, append=False, force=False, scope="Library", *ids): @@ -897,6 +939,7 @@ def compact(self): try: args = ["--compact", f"--lib={self.location}"] self.session._Session__call_console(args) + return self.location except RuntimeError as e: raise RuntimeError(f"Failed to compact library with the following " diff --git a/pysyncrosim/project.py b/pysyncrosim/project.py index 52e3b7f..915ac23 100644 --- a/pysyncrosim/project.py +++ b/pysyncrosim/project.py @@ -297,25 +297,39 @@ def datasheets(self, name=None, summary=True, optional=False, empty=False, return_hidden, self.pid) return self.__datasheets - def delete(self, scenario=None, force=False): + def delete(self, scenario=None, datasheet=None, ids=None, + force=False): """ - Deletes a Project or Scenario. + Deletes a Project, Scenario, or data from a Project scope. Parameters ---------- scenario : Scenario, String, or Int, optional Scenario to delete. The default is None. + datasheet : String, optional + Name of the datasheet to delete data from. The default is None. + ids : Int or String, optional + IDs of the rows to delete. If None, deletes all data. The default is + None. force : Logical, optional If True, does not prompt the user to confirm deletion. The default is False. - + Returns ------- None. """ + if datasheet is not None: + self.library.delete(datasheet=datasheet, pid=self.pid, + ids=ids, force=force) - self.library.delete(project=self, scenario=scenario, force=force) + elif scenario is not None: + self.library.delete(scenario = scenario, force = force) + + else: + self.library.delete(project=self, force=force) + def save_datasheet(self, name, data, append=True, force=False): """ diff --git a/pysyncrosim/scenario.py b/pysyncrosim/scenario.py index 6d7c5b1..6c764da 100644 --- a/pysyncrosim/scenario.py +++ b/pysyncrosim/scenario.py @@ -513,12 +513,17 @@ def save_datasheet(self, name, data, append=False): """ self.library.save_datasheet(name, data, append, False, "Scenario", self.sid) - def delete(self, force=False): + def delete(self, datasheet=None, ids=None, force=False): """ - Deletes a Scenario. + Deletes a Scenario or data from a Scenario scope. Parameters ---------- + datasheet : String, optional + Name of the datasheet to delete data from. The default is None + ids : Int or String, optional + IDs of the rows to delete. If None, deletes all data. The default is + None. force : Logical, optional If True, does not ask the user for permission to delete the Scenario. The default is False. @@ -528,8 +533,14 @@ def delete(self, force=False): None. """ - - self.library.delete(project=self.project, scenario=self, force=force) + + if datasheet is not None: + self.library.delete(datasheet=datasheet, sid=self.sid, + ids=ids, force=force) + + else: + self.library.delete(project=self.project, scenario=self, + force=force) def copy(self, name=None): """ diff --git a/tests/test_pysyncrosim.py b/tests/test_pysyncrosim.py index cb9bf69..da8fce3 100644 --- a/tests/test_pysyncrosim.py +++ b/tests/test_pysyncrosim.py @@ -349,17 +349,34 @@ def test_library_delete(): with pytest.raises(TypeError, match="force must be a Logical"): myLibrary.delete(force="True") - with pytest.raises(ValueError, match="Project ID 2 does not exist"): + with pytest.raises(ValueError, match="project 2 does not exist"): myLibrary.delete(project=2) with pytest.raises(ValueError, match="project dne does not exist"): myLibrary.delete(project="dne") - with pytest.raises(ValueError, match="Scenario ID 50 does not exist"): + with pytest.raises(ValueError, match="scenario 50 does not exist"): myLibrary.delete(scenario=50) with pytest.raises(ValueError, match="scenario dne does not exist"): myLibrary.delete(scenario="dne") + + with pytest.raises(TypeError, match="folder must be a Folder instance or Integer"): + myLibrary.delete(folder="folder") + + with pytest.raises(ValueError, match="Folder ID 50 does not exist"): + myLibrary.delete(folder=50, force=True) + + myProject = myLibrary.projects(name="test") + myFolder = myProject.folders(folder="test_folder") + myFolder2 = myProject.folders(folder="test_folder2") + fid = myFolder2.folder_id + + myLibrary.delete(folder=myFolder, force=True) + assert myFolder.folder_id not in myLibrary.folders()["Id"].values + + myLibrary.delete(folder=fid, force=True) + assert fid not in myLibrary.folders()["Id"].values myLibrary.delete(project="test", force=True) assert myLibrary._Library__projects.empty @@ -368,6 +385,74 @@ def test_library_delete(): myLibrary.scenarios(name="test") myLibrary.delete(scenario="test", force=True) assert "test" not in myLibrary.scenarios().Name.values + + myLibrary.delete(force=True) + assert not os.path.exists(lib_path) + + with pytest.raises( + ValueError, + match="Library not found:"): + myLibrary.delete(force=True) + + +def test_delete_datasheet(): + + mySession = ps.Session(session_path) + myLibrary = ps.library(name=lib_path, overwrite=True, + packages=["stsim"], session=mySession) + myProject = myLibrary.projects(name="test") + myScenario = myLibrary.scenarios(name="test") + myScenario2 = myLibrary.scenarios(name="test2") + test_data = pd.DataFrame({ + "Name": ["a1", "a2", "a3"], + "Id": [1, 2, 3], + "Description": ["test1", "test2", "test3"] + }) + + + with pytest.raises(TypeError, match="datasheet must be a String"): + myLibrary.delete(datasheet=1) + + with pytest.raises(TypeError, match="pid must be an Integer"): + myLibrary.delete(datasheet="core_Backup", pid="1") + + with pytest.raises(TypeError, match="sid must be an Integer"): + myLibrary.delete(datasheet="core_Backup", sid="1") + + with pytest.raises(ValueError, match="datasheet name is required"): + myLibrary.delete(datasheet="") + + # Add datasheet to project and test delete from project using Library class + myProject.save_datasheet(name="stsim_Stratum", data=test_data) + assert len(myProject.datasheets(name="stsim_Stratum")) == 3 + myLibrary.delete(datasheet="stsim_Stratum", pid=myProject.pid, force=True) + assert myProject.datasheets(name="stsim_Stratum").empty + + # Test delete datasheet from scenario using Library class + myLibrary.delete(datasheet="stsim_RunControl", + sid=myScenario.sid, force=True) + assert myScenario.datasheets(name="stsim_RunControl").empty + + # Test delete datasheet from project using Project class + myProject.save_datasheet(name="stsim_Stratum", data=test_data) + assert len(myProject.datasheets(name="stsim_Stratum")) == 3 + myProject.delete(datasheet="stsim_Stratum", force=True) + assert myProject.datasheets(name="stsim_Stratum").empty + + # Test delete datasheet from scenario using Scenario class + myScenario2.delete(datasheet="stsim_RunControl", force=True) + assert myScenario2.datasheets(name="stsim_RunControl").empty + + # Test delete datasheet by row ID + myProject.save_datasheet(name="stsim_Stratum", data=test_data) + saved_data = myProject.datasheets(name="stsim_Stratum", include_key=True) + + ids_to_delete = f"{saved_data.iloc[0]['StratumId']},{saved_data.iloc[1]['StratumId']}" + myLibrary.delete(datasheet="stsim_Stratum", pid=myProject.pid, + ids=ids_to_delete, force=True) + remaining_data = myProject.datasheets(name="stsim_Stratum") + assert len(remaining_data) == 1 + assert remaining_data.iloc[0]["Name"] == "a3" def test_library_save_datasheet():