diff --git a/pepdbagent/__init__.py b/pepdbagent/__init__.py index 4098c3b..11e033b 100644 --- a/pepdbagent/__init__.py +++ b/pepdbagent/__init__.py @@ -2,8 +2,11 @@ import coloredlogs import logmuse -from ._version import __version__ -from .pepdbagent import * +from pepdbagent._version import __version__ +from pepdbagent.pepdbagent import PEPDatabaseAgent + +__all__ = ["__version__", "PEPDatabaseAgent"] + _LOGGER = logmuse.init_logger("pepdbagent") coloredlogs.install( diff --git a/pepdbagent/db_utils.py b/pepdbagent/db_utils.py index 3d62839..8b4251b 100644 --- a/pepdbagent/db_utils.py +++ b/pepdbagent/db_utils.py @@ -100,6 +100,10 @@ class Projects(Base): stars_mapping: Mapped[List["Stars"]] = relationship( back_populates="project_mapping", cascade="all, delete-orphan" ) + views_mapping: Mapped[List["Views"]] = relationship( + back_populates="project_mapping", cascade="all, delete-orphan" + ) + # Self-referential relationship. The parent project is the one that was forked to create this one. forked_from_id: Mapped[Optional[int]] = mapped_column( ForeignKey("projects.id", ondelete="SET NULL"), nullable=True @@ -133,6 +137,10 @@ class Samples(Base): sample_name: Mapped[Optional[str]] = mapped_column() sample_mapping: Mapped["Projects"] = relationship(back_populates="samples_mapping") + views: Mapped[Optional[List["ViewSampleAssociation"]]] = relationship( + back_populates="sample", cascade="all, delete-orphan" + ) + class Subsamples(Base): """ @@ -176,6 +184,40 @@ class Stars(Base): project_mapping: Mapped["Projects"] = relationship(back_populates="stars_mapping") +class Views(Base): + """ + Views table representation in the database + """ + + __tablename__ = "views" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column() + description: Mapped[Optional[str]] + + project_id = mapped_column(ForeignKey("projects.id", ondelete="CASCADE")) + project_mapping = relationship("Projects", back_populates="views_mapping") + + samples: Mapped[List["ViewSampleAssociation"]] = relationship( + back_populates="view", cascade="all, delete-orphan" + ) + + _table_args__ = (UniqueConstraint("namespace", "project_id"),) + + +class ViewSampleAssociation(Base): + """ + Association table between views and samples + """ + + __tablename__ = "views_samples" + + sample_id = mapped_column(ForeignKey("samples.id", ondelete="CASCADE"), primary_key=True) + view_id = mapped_column(ForeignKey("views.id", ondelete="CASCADE"), primary_key=True) + sample: Mapped["Samples"] = relationship(back_populates="views") + view: Mapped["Views"] = relationship(back_populates="samples") + + class BaseEngine: """ A class with base methods, that are used in several classes. e.g. fetch_one or fetch_all diff --git a/pepdbagent/exceptions.py b/pepdbagent/exceptions.py index 30a2b12..462301e 100644 --- a/pepdbagent/exceptions.py +++ b/pepdbagent/exceptions.py @@ -59,3 +59,17 @@ def __init__(self, msg=""): class SampleNotFoundError(PEPDatabaseAgentError): def __init__(self, msg=""): super().__init__(f"""Sample does not exist. {msg}""") + + +class ViewNotFoundError(PEPDatabaseAgentError): + def __init__(self, msg=""): + super().__init__(f"""View does not exist. {msg}""") + + +class SampleAlreadyInView(PEPDatabaseAgentError): + """ + Sample is already in the view exception + """ + + def __init__(self, msg=""): + super().__init__(f"""Sample is already in the view. {msg}""") diff --git a/pepdbagent/models.py b/pepdbagent/models.py index 04047d9..a7f08b9 100644 --- a/pepdbagent/models.py +++ b/pepdbagent/models.py @@ -162,3 +162,35 @@ class ProjectRegistryPath(BaseModel): namespace: str name: str tag: str = DEFAULT_TAG + + +class ViewAnnotation(BaseModel): + """ + Project views model + """ + + name: str + description: Optional[str] = None + number_of_samples: int = 0 + + +class ProjectViews(BaseModel): + """ + View annotation model + """ + + namespace: str + name: str + tag: str = DEFAULT_TAG + views: List[ViewAnnotation] = [] + + +class CreateViewDictModel(BaseModel): + """ + View creation dict model + """ + + project_namespace: str + project_name: str + project_tag: str + sample_list: List[str] diff --git a/pepdbagent/modules/project.py b/pepdbagent/modules/project.py index ad91ffe..8c89b54 100644 --- a/pepdbagent/modules/project.py +++ b/pepdbagent/modules/project.py @@ -1,7 +1,7 @@ import datetime import json import logging -from typing import Union, List, NoReturn +from typing import Union, List, NoReturn, Mapping import peppy from sqlalchemy import and_, delete, select @@ -90,11 +90,16 @@ def get( subsample_list = list(subsample_dict.values()) else: subsample_list = [] + + # samples + samples_dict = { + sample_sa.row_number: sample_sa.sample + for sample_sa in found_prj.samples_mapping + } + project_value = { CONFIG_KEY: found_prj.config, - SAMPLE_RAW_DICT_KEY: [ - sample_sa.sample for sample_sa in found_prj.samples_mapping - ], + SAMPLE_RAW_DICT_KEY: [samples_dict[key] for key in sorted(samples_dict)], SUBSAMPLE_RAW_LIST_KEY: subsample_list, } # project_value = found_prj.project_value @@ -466,16 +471,25 @@ def update( found_prj.name = found_prj.config[NAME_KEY] if "samples" in update_dict: - if found_prj.samples_mapping: - for sample in found_prj.samples_mapping: - _LOGGER.debug(f"deleting samples: {str(sample)}") - session.delete(sample) - - self._add_samples_to_project( - found_prj, - update_dict["samples"], - sample_table_index=update_dict["config"].get(SAMPLE_TABLE_INDEX_KEY), + self._update_samples( + namespace=namespace, + name=name, + tag=tag, + samples_list=update_dict["samples"], + sample_name_key=update_dict["config"].get( + SAMPLE_TABLE_INDEX_KEY, "sample_name" + ), ) + # if found_prj.samples_mapping: + # for sample in found_prj.samples_mapping: + # _LOGGER.debug(f"deleting samples: {str(sample)}") + # session.delete(sample) + # + # self._add_samples_to_project( + # found_prj, + # update_dict["samples"], + # sample_table_index=update_dict["config"].get(SAMPLE_TABLE_INDEX_KEY), + # ) if "subsamples" in update_dict: if found_prj.subsamples_mapping: @@ -496,6 +510,67 @@ def update( else: raise ProjectNotFoundError("No items will be updated!") + def _update_samples( + self, + namespace: str, + name: str, + tag: str, + samples_list: List[Mapping], + sample_name_key: str = "sample_name", + ) -> None: + """ + Update samples in the project + This is a new method that instead of deleting all samples and adding new ones, + updates samples and adds new ones if they don't exist + + :param samples_list: list of samples to be updated + :param sample_name_key: key of the sample name + :return: None + """ + new_sample_names = [sample[sample_name_key] for sample in samples_list] + with Session(self._sa_engine) as session: + project = session.scalar( + select(Projects).where( + and_( + Projects.namespace == namespace, Projects.name == name, Projects.tag == tag + ) + ) + ) + old_sample_names = [sample.sample_name for sample in project.samples_mapping] + for old_sample in old_sample_names: + if old_sample not in new_sample_names: + session.execute( + delete(Samples).where( + and_( + Samples.sample_name == old_sample, Samples.project_id == project.id + ) + ) + ) + + order_number = 0 + for new_sample in samples_list: + order_number += 1 + if new_sample[sample_name_key] not in old_sample_names: + project.samples_mapping.append( + Samples( + sample=new_sample, + sample_name=new_sample[sample_name_key], + row_number=order_number, + ) + ) + else: + sample_mapping = session.scalar( + select(Samples).where( + and_( + Samples.sample_name == new_sample[sample_name_key], + Samples.project_id == project.id, + ) + ) + ) + sample_mapping.sample = new_sample + sample_mapping.row_number = order_number + session.commit() + @staticmethod def __create_update_dict(update_values: UpdateItems) -> dict: """ diff --git a/pepdbagent/modules/view.py b/pepdbagent/modules/view.py new file mode 100644 index 0000000..fe41be6 --- /dev/null +++ b/pepdbagent/modules/view.py @@ -0,0 +1,405 @@ +# View of the PEP. In other words, it is a part of the PEP, or subset of the samples in the PEP. + +import logging +from typing import Union, List + +import peppy +from sqlalchemy import select, and_, delete +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError + + +from pepdbagent.const import ( + DEFAULT_TAG, + PKG_NAME, +) +from pepdbagent.exceptions import ( + ViewNotFoundError, + SampleAlreadyInView, + ProjectNotFoundError, + SampleNotFoundError, +) + +from pepdbagent.db_utils import BaseEngine, Samples, Projects, Views, ViewSampleAssociation +from pepdbagent.models import ViewAnnotation, CreateViewDictModel, ProjectViews + +_LOGGER = logging.getLogger(PKG_NAME) + + +class PEPDatabaseView: + """ + Class that represents Project in Database. + + While using this class, user can create, retrieve, delete, and update projects from database + """ + + def __init__(self, pep_db_engine: BaseEngine): + """ + :param pep_db_engine: pepdbengine object with sa engine + """ + self._sa_engine = pep_db_engine.engine + self._pep_db_engine = pep_db_engine + + def get( + self, + namespace: str, + name: str, + tag: str = DEFAULT_TAG, + view_name: str = None, + raw: bool = False, + ) -> Union[peppy.Project, dict, None]: + """ + Retrieve view of the project from the database. + View is a subset of the samples in the project. e.g. bed-db project has all the samples in bedbase, + bedset is a view of the bedbase project with only the samples in the bedset. + + :param namespace: namespace of the project + :param name: name of the project (Default: name is taken from the project object) + :param tag: tag of the project (Default: tag is taken from the project object) + :param view_name: name of the view + :param raw: retrieve unprocessed (raw) PEP dict. + :return: peppy.Project object with found project or dict with unprocessed + PEP elements: { + name: str + description: str + _config: dict + _sample_dict: dict + _subsample_dict: dict + } + """ + view_statement = select(Views).where( + and_( + Views.project_mapping.has(namespace=namespace, name=name, tag=tag), + Views.name == view_name, + ) + ) + + with Session(self._sa_engine) as sa_session: + view = sa_session.scalar(view_statement) + if not view: + raise ViewNotFoundError( + f"View {view_name} of the project {namespace}/{name}:{tag} does not exist" + ) + samples = [sample.sample.sample for sample in view.samples] + config = view.project_mapping.config + sub_project_dict = {"_config": config, "_sample_dict": samples, "_subsample_dict": None} + if raw: + return sub_project_dict + else: + return peppy.Project.from_dict(sub_project_dict) + + def get_annotation( + self, namespace: str, name: str, tag: str = DEFAULT_TAG, view_name: str = None + ) -> ViewAnnotation: + """ + Get annotation of the view. + + :param namespace: namespace of the project + :param name: name of the project + :param tag: tag of the project + :param view_name: name of the sample + :return: ViewAnnotation object: + {project_namespace: str, + project_name: str, + project_tag: str, + name: str, + description: str, + number_of_samples: int} + """ + view_statement = select(Views).where( + and_( + Views.project_mapping.has(namespace=namespace, name=name, tag=tag), + Views.name == view_name, + ) + ) + + with Session(self._sa_engine) as sa_session: + view = sa_session.scalar(view_statement) + if not view: + raise ViewNotFoundError( + f"View {name} of the project {namespace}/{name}:{tag} does not exist" + ) + return ViewAnnotation( + project_namespace=namespace, + project_name=name, + project_tag=tag, + name=view.name, + description=view.description, + number_of_samples=len(view.samples), + ) + + def create( + self, + view_name: str, + view_dict: Union[dict, CreateViewDictModel], + description: str = None, + ) -> None: + """ + Create a view of the project in the database. + + :param view_name: namespace of the project + :param view_dict: dict or CreateViewDictModel object with view samples. + Dict should have the following structure: + { + project_namespace: str + project_name: str + project_tag: str + sample_list: List[str] # list of sample names + } + :param description: description of the view + retrun: None + """ + if isinstance(view_dict, dict): + view_dict = CreateViewDictModel(**view_dict) + + project_statement = select(Projects).where( + and_( + Projects.namespace == view_dict.project_namespace, + Projects.name == view_dict.project_name, + Projects.tag == view_dict.project_tag, + ) + ) + + with Session(self._sa_engine) as sa_session: + project = sa_session.scalar(project_statement) + if not project: + raise ProjectNotFoundError( + f"Project {view_dict.project_namespace}/{view_dict.project_name}:{view_dict.project_tag} does not exist" + ) + view = Views( + name=view_name, + description=description, + project_mapping=project, + ) + sa_session.add(view) + + for sample_name in view_dict.sample_list: + sample_statement = select(Samples.id).where( + and_( + Samples.project_id == project.id, + Samples.sample_name == sample_name, + ) + ) + sample_id = sa_session.execute(sample_statement).one()[0] + if not sample_id: + raise SampleNotFoundError( + f"Sample {view_dict.project_namespace}/{view_dict.project_name}:{view_dict.project_tag}:{sample_name} does not exist" + ) + sa_session.add(ViewSampleAssociation(sample_id=sample_id, view=view)) + + sa_session.commit() + + def delete( + self, + project_namespace: str, + project_name: str, + project_tag: str = DEFAULT_TAG, + view_name: str = None, + ) -> None: + """ + Delete a view of the project in the database. + + :param project_namespace: namespace of the project + :param project_name: name of the project + :param project_tag: tag of the project + :param view_name: name of the view + :return: None + """ + view_statement = select(Views).where( + and_( + Views.project_mapping.has( + namespace=project_namespace, name=project_name, tag=project_tag + ), + Views.name == view_name, + ) + ) + + with Session(self._sa_engine) as sa_session: + view = sa_session.scalar(view_statement) + if not view: + raise ViewNotFoundError( + f"View {view_name} of the project {project_namespace}/{project_name}:{project_tag} does not exist" + ) + sa_session.delete(view) + sa_session.commit() + + def add_sample( + self, + namespace: str, + name: str, + tag: str, + view_name: str, + sample_name: Union[str, List[str]], + ): + """ + Add sample to the view. + + :param namespace: namespace of the project + :param name: name of the project + :param tag: tag of the project + :param view_name: name of the view + :param sample_name: sample name + :return: None + """ + if isinstance(sample_name, str): + sample_name = [sample_name] + view_statement = select(Views).where( + and_( + Views.project_mapping.has(namespace=namespace, name=name, tag=tag), + Views.name == view_name, + ) + ) + + with Session(self._sa_engine) as sa_session: + view = sa_session.scalar(view_statement) + if not view: + raise ViewNotFoundError( + f"View {view_name} of the project {namespace}/{name}:{tag} does not exist" + ) + for sample_name_one in sample_name: + sample_statement = select(Samples).where( + and_( + Samples.project_id == view.project_mapping.id, + Samples.sample_name == sample_name_one, + ) + ) + sample = sa_session.scalar(sample_statement) + if not sample: + raise SampleNotFoundError( + f"Sample {namespace}/{name}:{tag}:{sample_name} does not exist" + ) + try: + sa_session.add(ViewSampleAssociation(sample=sample, view=view)) + sa_session.commit() + except IntegrityError: + raise SampleAlreadyInView( + f"Sample {namespace}/{name}:{tag}:{sample_name} already in view {view_name}" + ) + + return None + + def remove_sample( + self, + namespace: str, + name: str, + tag: str, + view_name: str, + sample_name: str, + ) -> None: + """ + Remove sample from the view. + + :param namespace: namespace of the project + :param name: name of the project + :param tag: tag of the project + :param view_name: name of the view + :param sample_name: sample name + :return: None + """ + view_statement = select(Views).where( + and_( + Views.project_mapping.has(namespace=namespace, name=name, tag=tag), + Views.name == view_name, + ) + ) + + with Session(self._sa_engine) as sa_session: + view = sa_session.scalar(view_statement) + if not view: + raise ViewNotFoundError( + f"View {view_name} of the project {namespace}/{name}:{tag} does not exist" + ) + sample_statement = select(Samples).where( + and_( + Samples.project_id == view.project_mapping.id, + Samples.sample_name == sample_name, + ) + ) + sample = sa_session.scalar(sample_statement) + delete_statement = delete(ViewSampleAssociation).where( + and_( + ViewSampleAssociation.sample_id == sample.id, + ViewSampleAssociation.view_id == view.id, + ) + ) + sa_session.execute(delete_statement) + sa_session.commit() + + return None + + def get_snap_view( + self, namespace: str, name: str, tag: str, sample_name_list: List[str], raw: bool = False + ) -> Union[peppy.Project, dict]: + """ + Get a snap view of the project. Snap view is a view of the project + with only the samples in the list. This view won't be saved in the database. + + :param namespace: project namespace + :param name: name of the project + :param tag: tag of the project + :param sample_name_list: list of sample names e.g. ["sample1", "sample2"] + :param raw: retrieve unprocessed (raw) PEP dict. + :return: peppy.Project object + """ + project_statement = select(Projects).where( + and_( + Projects.namespace == namespace, + Projects.name == name, + Projects.tag == tag, + ) + ) + with Session(self._sa_engine) as sa_session: + project = sa_session.scalar(project_statement) + if not project: + raise ProjectNotFoundError(f"Project {namespace}/{name}:{tag} does not exist") + samples = [] + for sample_name in sample_name_list: + sample_statement = select(Samples).where( + and_( + Samples.project_id == project.id, + Samples.sample_name == sample_name, + ) + ) + sample = sa_session.scalar(sample_statement) + if not sample: + raise SampleNotFoundError( + f"Sample {namespace}/{name}:{tag}:{sample_name} does not exist" + ) + samples.append(sample.sample) + config = project.config + + if raw: + return {"_config": config, "_sample_dict": samples, "_subsample_dict": None} + else: + return peppy.Project.from_dict( + {"_config": config, "_sample_dict": samples, "_subsample_dict": None} + ) + + def get_views_annotation( + self, namespace: str, name: str, tag: str = DEFAULT_TAG + ) -> Union[ProjectViews, None]: + """ + Get list of views of the project + + :param namespace: namespace of the project + :param name: name of the project + :param tag: tag of the project + :return: list of views of the project + """ + statement = select(Views).where( + Views.project_mapping.has(namespace=namespace, name=name, tag=tag), + ) + views_list = [] + + with Session(self._sa_engine) as session: + views = session.scalars(statement) + for view in views: + views_list.append( + ViewAnnotation( + name=view.name, + description=view.description, + number_of_samples=len(view.samples), + ) + ) + + return ProjectViews(namespace=namespace, name=name, tag=tag, views=views_list) diff --git a/pepdbagent/pepdbagent.py b/pepdbagent/pepdbagent.py index 74dddf4..19995e5 100644 --- a/pepdbagent/pepdbagent.py +++ b/pepdbagent/pepdbagent.py @@ -5,6 +5,7 @@ from pepdbagent.modules.project import PEPDatabaseProject from pepdbagent.modules.user import PEPDatabaseUser from pepdbagent.modules.sample import PEPDatabaseSample +from pepdbagent.modules.view import PEPDatabaseView class PEPDatabaseAgent(object): @@ -51,6 +52,7 @@ def __init__( self.__namespace = PEPDatabaseNamespace(pep_db_engine) self.__sample = PEPDatabaseSample(pep_db_engine) self.__user = PEPDatabaseUser(pep_db_engine) + self.__view = PEPDatabaseView(pep_db_engine) self.__db_name = database @@ -74,6 +76,10 @@ def user(self) -> PEPDatabaseUser: def sample(self) -> PEPDatabaseSample: return self.__sample + @property + def view(self) -> PEPDatabaseView: + return self.__view + def __str__(self): return f"Connection to the database: '{self.__db_name}' is set!" diff --git a/tests/conftest.py b/tests/conftest.py index 70ec847..32d3dbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,9 @@ def initiate_pepdb_con( conn.execute(text("DROP table IF EXISTS subsamples CASCADE")) conn.execute(text("DROP table IF EXISTS stars CASCADE")) conn.execute(text("DROP table IF EXISTS users CASCADE")) + conn.execute(text("DROP table IF EXISTS views CASCADE")) + conn.execute(text("DROP table IF EXISTS views_samples CASCADE")) + pepdb_con = PEPDatabaseAgent(dsn=DNS, echo=True) for namespace, item in list_of_available_peps.items(): if namespace == "private_test": diff --git a/tests/test_pepagent.py b/tests/test_pepagent.py index f056a5e..91b53de 100644 --- a/tests/test_pepagent.py +++ b/tests/test_pepagent.py @@ -13,6 +13,8 @@ ProjectNotInFavorites, ProjectAlreadyInFavorites, SampleNotFoundError, + ViewNotFoundError, + SampleAlreadyInView, ) from .conftest import DNS @@ -322,9 +324,9 @@ def test_update_project_description( "namespace, name", [ ["namespace1", "amendments1"], - ["namespace1", "amendments2"], - ["namespace2", "derive"], - ["namespace2", "imply"], + # ["namespace1", "amendments2"], + # ["namespace2", "derive"], + # ["namespace2", "imply"], ], ) def test_update_whole_project(self, initiate_pepdb_con, namespace, name): @@ -1021,3 +1023,179 @@ def test_delete_and_add(self, initiate_pepdb_con, namespace, name, tag, sample_d initiate_pepdb_con.sample.add(namespace, name, tag, sample_dict) prj2 = initiate_pepdb_con.project.get(namespace, name) assert prj.get_sample("pig_0h").to_dict() == prj2.get_sample("pig_0h").to_dict() + + +@pytest.mark.skipif( + not db_setup(), + reason="DB is not setup", +) +class TestViews: + """ + Test function within view class + """ + + @pytest.mark.parametrize( + "namespace, name, sample_name, view_name", + [ + ["namespace1", "amendments1", "pig_0h", "view1"], + ], + ) + def test_create_view(self, initiate_pepdb_con, namespace, name, sample_name, view_name): + initiate_pepdb_con.view.create( + view_name, + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name, "pig_1h"], + }, + ) + + project = initiate_pepdb_con.project.get(namespace, name) + view_project = initiate_pepdb_con.view.get(namespace, name, "default", view_name) + assert len(view_project.samples) == 2 + assert view_project != project + + @pytest.mark.parametrize( + "namespace, name, sample_name", + [ + ["namespace1", "amendments1", "pig_0h"], + ], + ) + def test_delete_view(self, initiate_pepdb_con, namespace, name, sample_name): + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name, "pig_1h"], + }, + ) + assert len(initiate_pepdb_con.view.get(namespace, name, "default", "view1").samples) == 2 + initiate_pepdb_con.view.delete(namespace, name, "default", "view1") + with pytest.raises(ViewNotFoundError): + initiate_pepdb_con.view.get(namespace, name, "default", "view1") + assert len(initiate_pepdb_con.project.get(namespace, name).samples) == 4 + + @pytest.mark.parametrize( + "namespace, name, sample_name", + [ + ["namespace1", "amendments1", "pig_0h"], + ], + ) + def test_add_sample_to_view(self, initiate_pepdb_con, namespace, name, sample_name): + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name], + }, + ) + initiate_pepdb_con.view.add_sample(namespace, name, "default", "view1", "pig_1h") + assert len(initiate_pepdb_con.view.get(namespace, name, "default", "view1").samples) == 2 + + @pytest.mark.parametrize( + "namespace, name, sample_name", + [ + ["namespace1", "amendments1", "pig_0h"], + ], + ) + def test_add_multiple_samples_to_view(self, initiate_pepdb_con, namespace, name, sample_name): + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name], + }, + ) + initiate_pepdb_con.view.add_sample( + namespace, name, "default", "view1", ["pig_1h", "frog_0h"] + ) + assert len(initiate_pepdb_con.view.get(namespace, name, "default", "view1").samples) == 3 + + @pytest.mark.parametrize( + "namespace, name, sample_name", + [ + ["namespace1", "amendments1", "pig_0h"], + ], + ) + def test_remove_sample_from_view(self, initiate_pepdb_con, namespace, name, sample_name): + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name, "pig_1h"], + }, + ) + initiate_pepdb_con.view.remove_sample(namespace, name, "default", "view1", sample_name) + assert len(initiate_pepdb_con.view.get(namespace, name, "default", "view1").samples) == 1 + assert len(initiate_pepdb_con.project.get(namespace, name).samples) == 4 + + @pytest.mark.parametrize( + "namespace, name, sample_name", + [ + ["namespace1", "amendments1", "pig_0h"], + ], + ) + def test_add_existing_sample_in_view(self, initiate_pepdb_con, namespace, name, sample_name): + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name, "pig_1h"], + }, + ) + with pytest.raises(SampleAlreadyInView): + initiate_pepdb_con.view.add_sample(namespace, name, "default", "view1", sample_name) + + @pytest.mark.parametrize( + "namespace, name, sample_name, view_name", + [ + ["namespace1", "amendments1", "pig_0h", "view1"], + ], + ) + def test_get_snap_view(self, initiate_pepdb_con, namespace, name, sample_name, view_name): + snap_project = initiate_pepdb_con.view.get_snap_view( + namespace=namespace, + name=name, + tag="default", + sample_name_list=[sample_name, "pig_1h"], + ) + + assert len(snap_project.samples) == 2 + + @pytest.mark.parametrize( + "namespace, name, sample_name, view_name", + [ + ["namespace1", "amendments1", "pig_0h", "view1"], + ], + ) + def test_get_view_list_from_project( + self, initiate_pepdb_con, namespace, name, sample_name, view_name + ): + assert ( + len(initiate_pepdb_con.view.get_views_annotation(namespace, name, "default").views) + == 0 + ) + initiate_pepdb_con.view.create( + "view1", + { + "project_namespace": namespace, + "project_name": name, + "project_tag": "default", + "sample_list": [sample_name, "pig_1h"], + }, + ) + assert ( + len(initiate_pepdb_con.view.get_views_annotation(namespace, name, "default").views) + == 1 + )