Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pepdbagent/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
DESCRIPTION_KEY = "description"
NAME_KEY = "name"

# from peppy.const import SAMPLE_RAW_DICT_KEY, SUBSAMPLE_RAW_LIST_KEY

DEFAULT_OFFSET = 0
DEFAULT_LIMIT = 100
Expand Down
4 changes: 2 additions & 2 deletions pepdbagent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import datetime
from typing import Dict, List, Optional, Union

from peppy.const import CONFIG_KEY, SAMPLE_RAW_DICT_KEY, SUBSAMPLE_RAW_LIST_KEY
from peprs.const import CONFIG_KEY, SAMPLE_RAW_DICT_KEY, SUBSAMPLE_RAW_DICT_KEY
from pydantic import BaseModel, ConfigDict, Field, field_validator

from pepdbagent.const import DEFAULT_TAG
Expand All @@ -14,7 +14,7 @@ class ProjectDict(BaseModel):
"""

config: dict = Field(alias=CONFIG_KEY)
subsample_list: Optional[Union[list, None]] = Field(alias=SUBSAMPLE_RAW_LIST_KEY)
subsample_list: Optional[Union[list, None]] = Field(alias=SUBSAMPLE_RAW_DICT_KEY)
sample_dict: list = Field(alias=SAMPLE_RAW_DICT_KEY)

model_config = ConfigDict(populate_by_name=True, extra="forbid")
Expand Down
73 changes: 36 additions & 37 deletions pepdbagent/modules/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
from typing import Dict, List, NoReturn, Union

import numpy as np
import peppy
from peppy.const import (
import peprs
from peprs.const import (
CONFIG_KEY,
SAMPLE_NAME_ATTR,
SAMPLE_RAW_DICT_KEY,
SAMPLE_TABLE_INDEX_KEY,
SUBSAMPLE_RAW_LIST_KEY,
SUBSAMPLE_RAW_DICT_KEY,
)

SAMPLE_NAME_ATTR = "sample_name"
SAMPLE_TABLE_INDEX_KEY = "sample_table_index"
from sqlalchemy import Select, and_, delete, select
Comment on lines +14 to 16
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SAMPLE_NAME_ATTR / SAMPLE_TABLE_INDEX_KEY are defined before the import block finishes (imports resume right after). This breaks standard import ordering. Move these constants below all imports (or into pepdbagent.const) so the file keeps a clean imports-then-code structure.

Copilot uses AI. Check for mistakes.
from sqlalchemy.exc import IntegrityError, NoResultFound
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -86,7 +87,7 @@ def get(
tag: str = DEFAULT_TAG,
raw: bool = True,
with_id: bool = False,
) -> Union[peppy.Project, dict, None]:
) -> Union[peprs.Project, dict, None]:
"""
Retrieve project from database by specifying namespace, name and tag

Expand All @@ -95,7 +96,7 @@ def get(
:param tag: tag (or version) of the project.
:param raw: retrieve unprocessed (raw) PEP dict.
:param with_id: retrieve project with id [default: False]
:return: peppy.Project object with found project or dict with unprocessed
:return: peprs.Project object with found project or dict with unprocessed
PEP elements: {
name: str
description: str
Expand Down Expand Up @@ -133,13 +134,13 @@ def get(
project_value = {
CONFIG_KEY: found_prj.config,
SAMPLE_RAW_DICT_KEY: sample_list,
SUBSAMPLE_RAW_LIST_KEY: subsample_list,
SUBSAMPLE_RAW_DICT_KEY: subsample_list,
}

if raw:
return project_value
else:
project_obj = peppy.Project().from_dict(project_value)
project_obj = peprs.Project.from_dict(project_value)
return project_obj

else:
Expand Down Expand Up @@ -224,13 +225,13 @@ def get_by_rp(
self,
registry_path: str,
raw: bool = False,
) -> Union[peppy.Project, dict, None]:
) -> Union[peprs.Project, dict, None]:
"""
Retrieve project from database by specifying project registry_path

:param registry_path: project registry_path [e.g. namespace/name:tag]
:param raw: retrieve unprocessed (raw) PEP dict.
:return: peppy.Project object with found project or dict with unprocessed
:return: peprs.Project object with found project or dict with unprocessed
PEP elements: {
name: str
description: str
Expand Down Expand Up @@ -296,7 +297,7 @@ def delete_by_rp(

def create(
self,
project: Union[peppy.Project, dict],
project: Union[peprs.Project, dict],
namespace: str,
name: str = None,
tag: str = DEFAULT_TAG,
Expand All @@ -312,14 +313,8 @@ def create(
Project with the key, that already exists won't be uploaded(but case, when argument
update is set True)

:param peppy.Project project: Project object that has to be uploaded to the DB
danger zone:
optionally, project can be a dictionary with PEP elements
({
_config: dict,
_sample_dict: Union[list, dict],
_subsample_list: list
})
:param project: peprs.Project object or dict with PEP elements
({config: dict, samples: list, subsamples: list})
:param namespace: namespace of the project (Default: 'other')
:param name: name of the project (Default: name is taken from the project object)
:param tag: tag (or version) of the project.
Expand All @@ -332,8 +327,8 @@ def create(
:param description: description of the project
:return: None
"""
if isinstance(project, peppy.Project):
proj_dict = project.to_dict(extended=True, orient="records")
if isinstance(project, peprs.Project):
proj_dict = project.to_dict(raw=True, by_sample=True)
elif isinstance(project, dict):
Comment on lines +330 to 332
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using peprs.Project.to_dict(raw=True, by_sample=True) will preserve peprs/polars-inferred column dtypes (e.g., numeric columns become ints). This appears to be a behavioral change vs the prior peppy-based behavior (tests now expect time == 0 instead of '0') and can be a breaking API change for clients expecting strings. Consider normalizing sample values to strings before persisting, or explicitly documenting this type change as part of the migration.

Copilot uses AI. Check for mistakes.
# verify if the dictionary has all necessary elements.
# samples should be always presented as list of dicts (orient="records"))
Expand All @@ -343,11 +338,14 @@ def create(
proj_dict = ProjectDict(**project).model_dump(by_alias=True)
else:
raise PEPDatabaseAgentError(
"Project has to be peppy.Project object or dictionary with PEP elements"
"Project has to be peprs.Project object or dictionary with PEP elements"
)

if not description:
description = project.get(description, "")
if isinstance(project, peprs.Project):
description = project.description or ""
else:
description = proj_dict.get(CONFIG_KEY, {}).get(DESCRIPTION_KEY, "")
proj_dict[CONFIG_KEY][DESCRIPTION_KEY] = description

namespace = namespace.lower()
Expand Down Expand Up @@ -442,8 +440,8 @@ def create(
),
)

if proj_dict[SUBSAMPLE_RAW_LIST_KEY]:
subsamples = proj_dict[SUBSAMPLE_RAW_LIST_KEY]
if proj_dict.get(SUBSAMPLE_RAW_DICT_KEY):
subsamples = proj_dict[SUBSAMPLE_RAW_DICT_KEY]
self._add_subsamples_to_project(new_prj, subsamples)

with Session(self._sa_engine) as session:
Expand Down Expand Up @@ -553,9 +551,9 @@ def _overwrite(
sample_table_index=project_dict[CONFIG_KEY].get(SAMPLE_TABLE_INDEX_KEY),
)

if project_dict[SUBSAMPLE_RAW_LIST_KEY]:
if project_dict.get(SUBSAMPLE_RAW_DICT_KEY):
self._add_subsamples_to_project(
found_prj, project_dict[SUBSAMPLE_RAW_LIST_KEY]
found_prj, project_dict[SUBSAMPLE_RAW_DICT_KEY]
)

session.commit()
Expand All @@ -579,7 +577,7 @@ def update(

:param update_dict: dict with update key->values. Dict structure:
{
project: Optional[peppy.Project]
project: Optional[peprs.Project]
is_private: Optional[bool]
tag: Optional[str]
name: Optional[str]
Expand All @@ -603,11 +601,11 @@ def update(
else:
if "project" in update_dict:
project_dict = update_dict.pop("project").to_dict(
extended=True, orient="records"
raw=True, by_sample=True
)
update_dict["config"] = project_dict[CONFIG_KEY]
update_dict["samples"] = project_dict[SAMPLE_RAW_DICT_KEY]
update_dict["subsamples"] = project_dict[SUBSAMPLE_RAW_LIST_KEY]
update_dict["subsamples"] = project_dict.get(SUBSAMPLE_RAW_DICT_KEY, [])

update_values = UpdateItems(**update_dict)

Expand Down Expand Up @@ -1174,7 +1172,8 @@ def get_samples(
).get(SAMPLE_RAW_DICT_KEY)
return (
self.get(namespace=namespace, name=name, tag=tag, raw=False, with_id=with_ids)
.sample_table.replace({np.nan: None})
.to_pandas()
.replace({np.nan: None})
.to_dict(orient="records")
)

Expand Down Expand Up @@ -1233,7 +1232,7 @@ def get_project_from_history(
history_id: int,
raw: bool = True,
with_id: bool = False,
) -> Union[dict, peppy.Project]:
) -> Union[dict, peprs.Project]:
"""
Get project sample history annotation by providing namespace, name, and tag

Expand Down Expand Up @@ -1319,13 +1318,13 @@ def get_project_from_history(
return {
CONFIG_KEY: project_config or project_mapping.config,
SAMPLE_RAW_DICT_KEY: ordered_samples_list,
SUBSAMPLE_RAW_LIST_KEY: self.get_subsamples(namespace, name, tag),
SUBSAMPLE_RAW_DICT_KEY: self.get_subsamples(namespace, name, tag),
}
return peppy.Project.from_dict(
return peprs.Project.from_dict(
pep_dictionary={
CONFIG_KEY: project_config or project_mapping.config,
SAMPLE_RAW_DICT_KEY: ordered_samples_list,
SUBSAMPLE_RAW_LIST_KEY: self.get_subsamples(namespace, name, tag),
SUBSAMPLE_RAW_DICT_KEY: self.get_subsamples(namespace, name, tag),
}
)

Expand Down Expand Up @@ -1440,7 +1439,7 @@ def restore(
with_id=True,
)
self.update(
update_dict={"project": peppy.Project.from_dict(restore_project)},
update_dict={"project": peprs.Project.from_dict(restore_project)},
namespace=namespace,
name=name,
tag=tag,
Expand Down
33 changes: 11 additions & 22 deletions pepdbagent/modules/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import logging
from typing import Union

import peppy
from peppy.const import SAMPLE_TABLE_INDEX_KEY
import peprs
from sqlalchemy import and_, select
from sqlalchemy.orm import Session
from sqlalchemy.orm.attributes import flag_modified
Expand Down Expand Up @@ -37,23 +36,16 @@ def get(
sample_name: str,
tag: str = DEFAULT_TAG,
raw: bool = True,
) -> Union[peppy.Sample, dict, None]:
) -> Union[peprs.Sample, dict, None]:
"""
Retrieve sample from the database using namespace, name, tag, and sample_name

:param namespace: namespace of the project
:param name: name of the project (Default: name is taken from the project object)
:param tag: tag (or version) of the project.
:param sample_name: sample_name of the sample
:param raw: return raw dict or peppy.Sample object [Default: True]
: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
}
:param raw: return raw dict or peprs.Sample object [Default: True]
:return: peprs.Sample object or raw dict
"""
statement_sample = select(Samples).where(
and_(
Expand Down Expand Up @@ -83,13 +75,10 @@ def get(
if result:
if not raw:
config = session.execute(project_config_statement).one_or_none()[0]
project = peppy.Project().from_dict(
project = peprs.Project.from_dict(
pep_dictionary={
"name": name,
"description": config.get("description"),
"_config": config,
"_sample_dict": [result.sample],
"_subsample_dict": None,
"config": config,
"samples": [result.sample],
}
)
return project.samples[0]
Expand Down Expand Up @@ -155,11 +144,11 @@ def update(
sample_mapping.sample.update(update_dict)
try:
sample_mapping.sample_name = sample_mapping.sample[
project_mapping.config.get(SAMPLE_TABLE_INDEX_KEY, "sample_name")
project_mapping.config.get("sample_table_index", "sample_name")
]
except KeyError:
raise KeyError(
Comment on lines 146 to 150
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sample_table_index is now referenced via the hard-coded string key here. To reduce duplication/typos and keep it consistent with the project module, consider using a shared constant (e.g., defined in pepdbagent.const) for this config key and reusing it in both the lookup and the error message.

Copilot uses AI. Check for mistakes.
f"Sample index key {project_mapping.config.get(SAMPLE_TABLE_INDEX_KEY, 'sample_name')} not found in sample dict"
f"Sample index key {project_mapping.config.get('sample_table_index', 'sample_name')} not found in sample dict"
)

# This line needed due to: https://github.com/sqlalchemy/sqlalchemy/issues/5218
Expand Down Expand Up @@ -206,11 +195,11 @@ def add(
project_mapping = session.scalar(project_statement)
try:
sample_name = sample_dict[
project_mapping.config.get(SAMPLE_TABLE_INDEX_KEY, "sample_name")
project_mapping.config.get("sample_table_index", "sample_name")
]
except KeyError:
raise KeyError(
f"Sample index key {project_mapping.config.get(SAMPLE_TABLE_INDEX_KEY, 'sample_name')} not found in sample dict"
f"Sample index key {project_mapping.config.get('sample_table_index', 'sample_name')} not found in sample dict"
)
statement = select(Samples).where(
and_(Samples.project_id == project_mapping.id, Samples.sample_name == sample_name)
Expand Down
Loading
Loading