Skip to content

Commit 407dc32

Browse files
authored
Merge pull request #113 from pepkit/veiw
Added views module
2 parents 6d9fd85 + 50110fc commit 407dc32

File tree

9 files changed

+776
-18
lines changed

9 files changed

+776
-18
lines changed

pepdbagent/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import coloredlogs
33
import logmuse
44

5-
from ._version import __version__
6-
from .pepdbagent import *
5+
from pepdbagent._version import __version__
6+
from pepdbagent.pepdbagent import PEPDatabaseAgent
7+
8+
__all__ = ["__version__", "PEPDatabaseAgent"]
9+
710

811
_LOGGER = logmuse.init_logger("pepdbagent")
912
coloredlogs.install(

pepdbagent/db_utils.py

+42
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ class Projects(Base):
100100
stars_mapping: Mapped[List["Stars"]] = relationship(
101101
back_populates="project_mapping", cascade="all, delete-orphan"
102102
)
103+
views_mapping: Mapped[List["Views"]] = relationship(
104+
back_populates="project_mapping", cascade="all, delete-orphan"
105+
)
106+
103107
# Self-referential relationship. The parent project is the one that was forked to create this one.
104108
forked_from_id: Mapped[Optional[int]] = mapped_column(
105109
ForeignKey("projects.id", ondelete="SET NULL"), nullable=True
@@ -133,6 +137,10 @@ class Samples(Base):
133137
sample_name: Mapped[Optional[str]] = mapped_column()
134138
sample_mapping: Mapped["Projects"] = relationship(back_populates="samples_mapping")
135139

140+
views: Mapped[Optional[List["ViewSampleAssociation"]]] = relationship(
141+
back_populates="sample", cascade="all, delete-orphan"
142+
)
143+
136144

137145
class Subsamples(Base):
138146
"""
@@ -176,6 +184,40 @@ class Stars(Base):
176184
project_mapping: Mapped["Projects"] = relationship(back_populates="stars_mapping")
177185

178186

187+
class Views(Base):
188+
"""
189+
Views table representation in the database
190+
"""
191+
192+
__tablename__ = "views"
193+
194+
id: Mapped[int] = mapped_column(primary_key=True)
195+
name: Mapped[str] = mapped_column()
196+
description: Mapped[Optional[str]]
197+
198+
project_id = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"))
199+
project_mapping = relationship("Projects", back_populates="views_mapping")
200+
201+
samples: Mapped[List["ViewSampleAssociation"]] = relationship(
202+
back_populates="view", cascade="all, delete-orphan"
203+
)
204+
205+
_table_args__ = (UniqueConstraint("namespace", "project_id"),)
206+
207+
208+
class ViewSampleAssociation(Base):
209+
"""
210+
Association table between views and samples
211+
"""
212+
213+
__tablename__ = "views_samples"
214+
215+
sample_id = mapped_column(ForeignKey("samples.id", ondelete="CASCADE"), primary_key=True)
216+
view_id = mapped_column(ForeignKey("views.id", ondelete="CASCADE"), primary_key=True)
217+
sample: Mapped["Samples"] = relationship(back_populates="views")
218+
view: Mapped["Views"] = relationship(back_populates="samples")
219+
220+
179221
class BaseEngine:
180222
"""
181223
A class with base methods, that are used in several classes. e.g. fetch_one or fetch_all

pepdbagent/exceptions.py

+14
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,17 @@ def __init__(self, msg=""):
5959
class SampleNotFoundError(PEPDatabaseAgentError):
6060
def __init__(self, msg=""):
6161
super().__init__(f"""Sample does not exist. {msg}""")
62+
63+
64+
class ViewNotFoundError(PEPDatabaseAgentError):
65+
def __init__(self, msg=""):
66+
super().__init__(f"""View does not exist. {msg}""")
67+
68+
69+
class SampleAlreadyInView(PEPDatabaseAgentError):
70+
"""
71+
Sample is already in the view exception
72+
"""
73+
74+
def __init__(self, msg=""):
75+
super().__init__(f"""Sample is already in the view. {msg}""")

pepdbagent/models.py

+32
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,35 @@ class ProjectRegistryPath(BaseModel):
162162
namespace: str
163163
name: str
164164
tag: str = DEFAULT_TAG
165+
166+
167+
class ViewAnnotation(BaseModel):
168+
"""
169+
Project views model
170+
"""
171+
172+
name: str
173+
description: Optional[str] = None
174+
number_of_samples: int = 0
175+
176+
177+
class ProjectViews(BaseModel):
178+
"""
179+
View annotation model
180+
"""
181+
182+
namespace: str
183+
name: str
184+
tag: str = DEFAULT_TAG
185+
views: List[ViewAnnotation] = []
186+
187+
188+
class CreateViewDictModel(BaseModel):
189+
"""
190+
View creation dict model
191+
"""
192+
193+
project_namespace: str
194+
project_name: str
195+
project_tag: str
196+
sample_list: List[str]

pepdbagent/modules/project.py

+88-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
import json
33
import logging
4-
from typing import Union, List, NoReturn
4+
from typing import Union, List, NoReturn, Mapping
55

66
import peppy
77
from sqlalchemy import and_, delete, select
@@ -90,11 +90,16 @@ def get(
9090
subsample_list = list(subsample_dict.values())
9191
else:
9292
subsample_list = []
93+
94+
# samples
95+
samples_dict = {
96+
sample_sa.row_number: sample_sa.sample
97+
for sample_sa in found_prj.samples_mapping
98+
}
99+
93100
project_value = {
94101
CONFIG_KEY: found_prj.config,
95-
SAMPLE_RAW_DICT_KEY: [
96-
sample_sa.sample for sample_sa in found_prj.samples_mapping
97-
],
102+
SAMPLE_RAW_DICT_KEY: [samples_dict[key] for key in sorted(samples_dict)],
98103
SUBSAMPLE_RAW_LIST_KEY: subsample_list,
99104
}
100105
# project_value = found_prj.project_value
@@ -466,16 +471,25 @@ def update(
466471
found_prj.name = found_prj.config[NAME_KEY]
467472

468473
if "samples" in update_dict:
469-
if found_prj.samples_mapping:
470-
for sample in found_prj.samples_mapping:
471-
_LOGGER.debug(f"deleting samples: {str(sample)}")
472-
session.delete(sample)
473-
474-
self._add_samples_to_project(
475-
found_prj,
476-
update_dict["samples"],
477-
sample_table_index=update_dict["config"].get(SAMPLE_TABLE_INDEX_KEY),
474+
self._update_samples(
475+
namespace=namespace,
476+
name=name,
477+
tag=tag,
478+
samples_list=update_dict["samples"],
479+
sample_name_key=update_dict["config"].get(
480+
SAMPLE_TABLE_INDEX_KEY, "sample_name"
481+
),
478482
)
483+
# if found_prj.samples_mapping:
484+
# for sample in found_prj.samples_mapping:
485+
# _LOGGER.debug(f"deleting samples: {str(sample)}")
486+
# session.delete(sample)
487+
#
488+
# self._add_samples_to_project(
489+
# found_prj,
490+
# update_dict["samples"],
491+
# sample_table_index=update_dict["config"].get(SAMPLE_TABLE_INDEX_KEY),
492+
# )
479493

480494
if "subsamples" in update_dict:
481495
if found_prj.subsamples_mapping:
@@ -496,6 +510,67 @@ def update(
496510
else:
497511
raise ProjectNotFoundError("No items will be updated!")
498512

513+
def _update_samples(
514+
self,
515+
namespace: str,
516+
name: str,
517+
tag: str,
518+
samples_list: List[Mapping],
519+
sample_name_key: str = "sample_name",
520+
) -> None:
521+
"""
522+
Update samples in the project
523+
This is a new method that instead of deleting all samples and adding new ones,
524+
updates samples and adds new ones if they don't exist
525+
526+
:param samples_list: list of samples to be updated
527+
:param sample_name_key: key of the sample name
528+
:return: None
529+
"""
530+
new_sample_names = [sample[sample_name_key] for sample in samples_list]
531+
with Session(self._sa_engine) as session:
532+
project = session.scalar(
533+
select(Projects).where(
534+
and_(
535+
Projects.namespace == namespace, Projects.name == name, Projects.tag == tag
536+
)
537+
)
538+
)
539+
old_sample_names = [sample.sample_name for sample in project.samples_mapping]
540+
for old_sample in old_sample_names:
541+
if old_sample not in new_sample_names:
542+
session.execute(
543+
delete(Samples).where(
544+
and_(
545+
Samples.sample_name == old_sample, Samples.project_id == project.id
546+
)
547+
)
548+
)
549+
550+
order_number = 0
551+
for new_sample in samples_list:
552+
order_number += 1
553+
if new_sample[sample_name_key] not in old_sample_names:
554+
project.samples_mapping.append(
555+
Samples(
556+
sample=new_sample,
557+
sample_name=new_sample[sample_name_key],
558+
row_number=order_number,
559+
)
560+
)
561+
else:
562+
sample_mapping = session.scalar(
563+
select(Samples).where(
564+
and_(
565+
Samples.sample_name == new_sample[sample_name_key],
566+
Samples.project_id == project.id,
567+
)
568+
)
569+
)
570+
sample_mapping.sample = new_sample
571+
sample_mapping.row_number = order_number
572+
session.commit()
573+
499574
@staticmethod
500575
def __create_update_dict(update_values: UpdateItems) -> dict:
501576
"""

0 commit comments

Comments
 (0)