Skip to content

Commit

Permalink
Fixed #104
Browse files Browse the repository at this point in the history
  • Loading branch information
khoroshevskyi committed Dec 10, 2023
1 parent c395cda commit a0a5269
Show file tree
Hide file tree
Showing 10 changed files with 375 additions and 5 deletions.
2 changes: 1 addition & 1 deletion pepdbagent/_version.py
@@ -1 +1 @@
__version__ = "0.6.0"
__version__ = "0.7.0dev0"
32 changes: 31 additions & 1 deletion pepdbagent/db_utils.py
Expand Up @@ -97,7 +97,9 @@ class Projects(Base):
subsamples_mapping: Mapped[List["Subsamples"]] = relationship(
back_populates="subsample_mapping", cascade="all, delete-orphan"
)

favorites_mapping: Mapped["Favorites"] = relationship(
back_populates="project_mapping", cascade="all, delete-orphan"
)
__table_args__ = (UniqueConstraint("namespace", "name", "tag"),)


Expand Down Expand Up @@ -130,6 +132,34 @@ class Subsamples(Base):
subsample_mapping: Mapped["Projects"] = relationship(back_populates="subsamples_mapping")


class User(Base):
"""
User table representation in the database
"""

__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True)
namespace: Mapped[str]
favorites_mapping: Mapped[List["Favorites"]] = relationship(
back_populates="user_mapping", cascade="all, delete-orphan"
)


class Favorites(Base):
"""
FavoriteProjects table representation in the database
"""

__tablename__ = "favorites"

# id: Mapped[int] = mapped_column(primary_key=True)
user_id = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), primary_key=True)
project_id = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), primary_key=True)
user_mapping: Mapped[List["User"]] = relationship(back_populates="favorites_mapping")
project_mapping: Mapped["Projects"] = relationship(back_populates="favorites_mapping")


class BaseEngine:
"""
A class with base methods, that are used in several classes. e.g. fetch_one or fetch_all
Expand Down
9 changes: 9 additions & 0 deletions pepdbagent/exceptions.py
Expand Up @@ -36,3 +36,12 @@ def __init__(self, msg=""):
class FilterError(PEPDatabaseAgentError):
def __init__(self, msg=""):
super().__init__(f"""pepdbagent filter error. {msg}""")


class ProjectNotInFavorites(PEPDatabaseAgentError):
"""
Project doesn't exist in favorites
"""

def __init__(self, msg=""):
super().__init__(f"""Project is not in favorites list. {msg}""")
12 changes: 12 additions & 0 deletions pepdbagent/models.py
Expand Up @@ -2,6 +2,8 @@
from typing import List, Optional, Union
from pydantic import BaseModel, Extra, Field, validator

from pepdbagent.const import DEFAULT_TAG


class AnnotationModel(BaseModel):
"""
Expand Down Expand Up @@ -147,3 +149,13 @@ class ListOfNamespaceInfo(BaseModel):
number_of_namespaces: int
limit: int
results: List[NamespaceInfo]


class ProjectRegistryPath(BaseModel):
"""
Project Namespace
"""

namespace: str
name: str
tag: str = DEFAULT_TAG
4 changes: 2 additions & 2 deletions pepdbagent/modules/namespace.py
@@ -1,5 +1,5 @@
import logging
from typing import List, Union
from typing import List, Union, Tuple

from sqlalchemy import distinct, func, or_, select, text
from sqlalchemy.sql.selectable import Select
Expand Down Expand Up @@ -141,7 +141,7 @@ def _count_namespace(self, search_str: str = None, admin_nsp: tuple = tuple()) -
def _add_condition(
statement: Select,
search_str: str = None,
admin_list: Union[str, List[str]] = None,
admin_list: Union[str, List[str], Tuple[str]] = None,
) -> Select:
"""
Add where clause to sqlalchemy statement (in namespace search)
Expand Down
20 changes: 20 additions & 0 deletions pepdbagent/modules/project.py
Expand Up @@ -363,6 +363,7 @@ def _overwrite(
found_prj.config = project_dict[CONFIG_KEY]
found_prj.description = description
found_prj.last_update_date = datetime.datetime.now(datetime.timezone.utc)
found_prj.pop = pop

# Deleting old samples and subsamples
if found_prj.samples_mapping:
Expand Down Expand Up @@ -604,3 +605,22 @@ def _add_subsamples_to_project(
projects_sa.subsamples_mapping.append(
Subsamples(subsample=sub_item, subsample_number=i, row_number=row_number)
)

def get_project_id(self, namespace: str, name: str, tag: str) -> Union[int, None]:
"""
Get Project id by providing namespace, name, and tag
:param namespace: project namespace
:param name: project name
:param tag: project tag
:return: projects id
"""
statement = select(Projects.id).where(
and_(Projects.namespace == namespace, Projects.name == name, Projects.tag == tag)
)
with Session(self._sa_engine) as session:
result = session.execute(statement).one_or_none()

if result:
return result[0]
return None
192 changes: 192 additions & 0 deletions pepdbagent/modules/user.py
@@ -0,0 +1,192 @@
import logging
from typing import Union

from sqlalchemy import and_, delete, select
from sqlalchemy.orm import Session

from pepdbagent.const import (
PKG_NAME,
)

from pepdbagent.db_utils import BaseEngine, User, Favorites
from pepdbagent.modules.project import PEPDatabaseProject
from pepdbagent.models import AnnotationList, AnnotationModel
from pepdbagent.exceptions import ProjectNotInFavorites


_LOGGER = logging.getLogger(PKG_NAME)


class PEPDatabaseUser:
"""
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 create_user(self, namespace: str) -> int:
"""
Create new user
:param namespace: user namespace
:return: user id
"""
new_user_raw = User(namespace=namespace)

with Session(self._sa_engine) as session:
session.add(new_user_raw)
session.commit()
user_id = new_user_raw.id
return user_id

def get_user_id(self, namespace: str) -> Union[int, None]:
"""
Get user id using username
:param namespace: user namespace
:return: user id
"""
statement = select(User.id).where(User.namespace == namespace)
with Session(self._sa_engine) as session:
result = session.execute(statement).one_or_none()

if result:
return result[0]
return None

def add_to_favorites(self, namespace, project_namespace, project_name, project_tag) -> None:
"""
Add project to favorites
:param namespace: namespace of the user
:param project_namespace: namespace of the project
:param project_name: name of the project
:param project_tag: tag of the project
:return: None
"""

project_id = PEPDatabaseProject(self._pep_db_engine).get_project_id(
project_namespace, project_name, project_tag
)
user_id = self.get_user_id(namespace)

if not user_id:
user_id = self.create_user(namespace)

new_favorites_raw = Favorites(user_id=user_id, project_id=project_id)

with Session(self._sa_engine) as session:
session.add(new_favorites_raw)
session.commit()
return None

def remove_from_favorites(
self, namespace: str, project_namespace: str, project_name: str, project_tag: str
) -> None:
"""
Remove project from favorites
:param namespace: namespace of the user
:param project_namespace: namespace of the project
:param project_name: name of the project
:param project_tag: tag of the project
:return: None
"""
_LOGGER.debug(
f"Removing project {project_name} from fProjectNotInFavoritesavorites for user {namespace}"
)
project_id = PEPDatabaseProject(self._pep_db_engine).get_project_id(
project_namespace, project_name, project_tag
)
user_id = self.get_user_id(namespace)
statement = delete(Favorites).where(
and_(
Favorites.user_id == user_id,
Favorites.project_id == project_id,
)
)

with Session(self._sa_engine) as session:
result = session.execute(statement)
session.commit()
row_count = result.rowcount
if row_count == 0:
raise ProjectNotInFavorites(
f"Project {project_namespace}/{project_name}:{project_tag} is not in favorites for user {namespace}"
)
return None

def get_favorites(self, namespace: str) -> AnnotationList:
"""
Get list of favorites for user
:param namespace: namespace of the user
:return: list of favorite projects with annotations
"""
_LOGGER.debug(f"Getting favorites for user {namespace}")
if not self.exists(namespace):
return AnnotationList(
count=0,
limit=0,
offset=0,
results=[],
)
statement = select(User).where(User.namespace == namespace)
with Session(self._sa_engine) as session:
query_result = session.scalar(statement)
number_of_projects = len([kk.project_mapping for kk in query_result.favorites_mapping])
project_list = []
for prj_list in query_result.favorites_mapping:
project_list.append(
AnnotationModel(
namespace=prj_list.project_mapping.namespace,
name=prj_list.project_mapping.name,
tag=prj_list.project_mapping.tag,
is_private=prj_list.project_mapping.private,
number_of_samples=prj_list.project_mapping.number_of_samples,
description=prj_list.project_mapping.description,
last_update_date=str(prj_list.project_mapping.last_update_date),
submission_date=str(prj_list.project_mapping.submission_date),
digest=prj_list.project_mapping.digest,
pep_schema=prj_list.project_mapping.pep_schema,
pop=prj_list.project_mapping.pop,
)
)
favorite_prj = AnnotationList(
count=number_of_projects,
limit=number_of_projects,
offset=0,
results=project_list,
)
return favorite_prj

def exists(
self,
namespace: str,
) -> bool:
"""
Check if user exists in the database.
:param namespace: project namespace
:return: Returning True if project exist
"""

statement = select(User.id)
statement = statement.where(
and_(
User.namespace == namespace,
)
)
found_prj = self._pep_db_engine.session_execute(statement).all()

if len(found_prj) > 0:
return True
else:
return False
6 changes: 6 additions & 0 deletions pepdbagent/pepdbagent.py
Expand Up @@ -3,6 +3,7 @@
from pepdbagent.modules.annotation import PEPDatabaseAnnotation
from pepdbagent.modules.namespace import PEPDatabaseNamespace
from pepdbagent.modules.project import PEPDatabaseProject
from pepdbagent.modules.user import PEPDatabaseUser


class PEPDatabaseAgent(object):
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(
self.__project = PEPDatabaseProject(pep_db_engine)
self.__annotation = PEPDatabaseAnnotation(pep_db_engine)
self.__namespace = PEPDatabaseNamespace(pep_db_engine)
self.__user = PEPDatabaseUser(pep_db_engine)

self.__db_name = database

Expand All @@ -62,6 +64,10 @@ def annotation(self):
def namespace(self):
return self.__namespace

@property
def user(self):
return self.__user

def __str__(self):
return f"Connection to the database: '{self.__db_name}' is set!"

Expand Down
2 changes: 2 additions & 0 deletions tests/conftest.py
Expand Up @@ -40,6 +40,8 @@ def initiate_pepdb_con(
conn.execute(text("DROP table IF EXISTS projects CASCADE"))
conn.execute(text("DROP table IF EXISTS samples CASCADE"))
conn.execute(text("DROP table IF EXISTS subsamples CASCADE"))
conn.execute(text("DROP table IF EXISTS favorites CASCADE"))
conn.execute(text("DROP table IF EXISTS users CASCADE"))
pepdb_con = PEPDatabaseAgent(dsn=DNS, echo=True)
for namespace, item in list_of_available_peps.items():
if namespace == "private_test":
Expand Down

0 comments on commit a0a5269

Please sign in to comment.