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
7 changes: 7 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.

## [0.7.0] - 2026-08-06
### Changed
- Replaced the `peppy` dependency with `peprs` (Rust-backed PEP object model).
`load_project`, `view.get`, `push`, and `upload` now operate on `peprs.Project`
objects instead of `peppy.Project`.
- Added `pyyaml` as a direct dependency (previously pulled in transitively via peppy).

## [0.5.1] - 2026-03-18
### Fixed
- Fixed saving project to pephub [#55](https://github.com/pepkit/pephubclient/issues/55)
Expand Down
6 changes: 6 additions & 0 deletions pephubclient/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

from pydantic import BaseModel, field_validator

# PEP config/sample keys not exported by peprs.const
NAME_KEY: str = "name"
DESC_KEY: str = "description"
CFG_SAMPLE_TABLE_KEY: str = "sample_table"
CFG_SUBSAMPLE_TABLE_KEY: str = "subsample_table"

DEFAULT_BASE_URL: str = "https://pephub-api.databio.org/"
PEPHUB_BASE_URL: str = os.getenv("PEPHUB_BASE_URL", default=DEFAULT_BASE_URL)
# PEPHUB_BASE_URL = "http://0.0.0.0:8000/"
Expand Down
38 changes: 20 additions & 18 deletions pephubclient/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,25 @@
from urllib.parse import urlencode

import pandas as pd
import peppy
import peprs
import requests
import yaml
from peppy.const import (
CFG_SAMPLE_TABLE_KEY,
CFG_SUBSAMPLE_TABLE_KEY,
from peprs.const import (
CONFIG_KEY,
DESC_KEY,
NAME_KEY,
SAMPLE_RAW_DICT_KEY,
SUBSAMPLE_RAW_LIST_KEY,
SUBSAMPLE_RAW_DICT_KEY,
)
from pydantic import ValidationError
from requests.exceptions import ConnectionError
from ubiquerg import parse_registry_path

from pephubclient.constants import RegistryPath
from pephubclient.constants import (
CFG_SAMPLE_TABLE_KEY,
CFG_SUBSAMPLE_TABLE_KEY,
DESC_KEY,
NAME_KEY,
RegistryPath,
)
from pephubclient.exceptions import (
BasePephubclientException,
PEPExistsError,
Expand Down Expand Up @@ -227,15 +229,15 @@ def _save_zip_pep(project: dict, zip_filepath: str, force: bool = False) -> None
project[SAMPLE_RAW_DICT_KEY]
).to_csv(index=False)

if project[SUBSAMPLE_RAW_LIST_KEY] is not None:
if not isinstance(project[SUBSAMPLE_RAW_LIST_KEY], list):
if project[SUBSAMPLE_RAW_DICT_KEY] is not None:
if not isinstance(project[SUBSAMPLE_RAW_DICT_KEY], list):
config[CFG_SUBSAMPLE_TABLE_KEY] = ["subsample_table1.csv"]
content_to_zip["subsample_table1.csv"] = pd.DataFrame(
project[SUBSAMPLE_RAW_LIST_KEY]
project[SUBSAMPLE_RAW_DICT_KEY]
).to_csv(index=False)
else:
config[CFG_SUBSAMPLE_TABLE_KEY] = []
for number, file in enumerate(project[SUBSAMPLE_RAW_LIST_KEY]):
for number, file in enumerate(project[SUBSAMPLE_RAW_DICT_KEY]):
file_name = f"subsample_table{number + 1}.csv"
config[CFG_SUBSAMPLE_TABLE_KEY].append(file_name)
content_to_zip[file_name] = pd.DataFrame(file).to_csv(index=False)
Expand Down Expand Up @@ -279,7 +281,7 @@ def full_path(fn: str) -> str:
sample_pandas = pd.DataFrame(project_dict.get(SAMPLE_RAW_DICT_KEY, {}))

subsample_list = [
pd.DataFrame(sub_a) for sub_a in project_dict.get(SUBSAMPLE_RAW_LIST_KEY) or []
pd.DataFrame(sub_a) for sub_a in project_dict.get(SUBSAMPLE_RAW_DICT_KEY) or []
]

filenames = []
Expand All @@ -305,8 +307,8 @@ def full_path(fn: str) -> str:


def save_pep(
project: dict | peppy.Project,
reg_path: str = None,
project: dict | peprs.Project,
reg_path: str | None = None,
force: bool = False,
project_path: str | None = None,
zip: bool = False,
Expand All @@ -323,10 +325,10 @@ def save_pep(
the current directory.
zip: If True, save project as zip file.
"""
if isinstance(project, peppy.Project):
project = project.to_dict(extended=True, orient="records")
if isinstance(project, peprs.Project):
project = project.to_dict(raw=True, by_sample=True)

project = ProjectDict(**project).model_dump(by_alias=True)
project = ProjectDict(**project).model_dump()

if not project_path:
project_path = os.getcwd()
Expand Down
12 changes: 7 additions & 5 deletions pephubclient/models.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import datetime

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


class ProjectDict(BaseModel):
"""
Project dict (raw) model.

Field names match the PEPHub raw-PEP payload and the peprs dialect
(``config``/``samples``/``subsamples``), so no aliases are needed.
"""

config: dict = Field(alias=CONFIG_KEY)
subsamples: list | None = Field(alias=SUBSAMPLE_RAW_LIST_KEY)
samples: list = Field(alias=SAMPLE_RAW_DICT_KEY)
config: dict
subsamples: list | None = None
samples: list

model_config = ConfigDict(populate_by_name=True, extra="allow")

Expand Down
2 changes: 1 addition & 1 deletion pephubclient/modules/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class PEPHubSample(RequestManager):
Class for managing samples in PEPhub.

Provides methods for getting, creating, updating and removing samples. This class
is not related to the peppy.Sample class.
is not related to the peprs.Sample class.
"""

def __init__(self, jwt_data: str | None = None) -> None:
Expand Down
10 changes: 5 additions & 5 deletions pephubclient/modules/view.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

import peppy
import peprs

from pephubclient.constants import (
PEPHUB_VIEW_SAMPLE_URL,
Expand Down Expand Up @@ -31,7 +31,7 @@ def __init__(self, jwt_data: str | None = None) -> None:

def get(
self, namespace: str, name: str, tag: str, view_name: str, raw: bool = False
) -> peppy.Project | dict:
) -> peprs.Project | dict:
"""
Get view from project in PEPhub.

Expand All @@ -43,7 +43,7 @@ def get(
raw: If True, return raw response.

Returns:
peppy.Project object or dictionary of the project (view).
peprs.Project object or dictionary of the project (view).
"""
url = self._build_view_request_url(
namespace=namespace, name=name, view_name=view_name
Expand All @@ -58,8 +58,8 @@ def get(
output = self.decode_response(response, output_json=True)
if raw:
return output
output = ProjectDict(**output).model_dump(by_alias=True)
return peppy.Project.from_dict(output)
output = ProjectDict(**output).model_dump()
return peprs.Project.from_dict(output)
elif response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError("View does not exist, or you are unauthorized.")
else:
Expand Down
61 changes: 26 additions & 35 deletions pephubclient/pephubclient.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
from typing import Literal

import peppy
import peprs
import urllib3
from peppy.const import (
CONFIG_KEY,
NAME_KEY,
SAMPLE_RAW_DICT_KEY,
SUBSAMPLE_RAW_LIST_KEY,
)
from peprs.const import CONFIG_KEY
from pydantic import ValidationError
from typing_extensions import deprecated
from ubiquerg import parse_registry_path

from pephubclient.constants import (
NAME_KEY,
PATH_TO_TOKEN_FILE,
RegistryPath,
ResponseStatusCodes,
Expand Down Expand Up @@ -119,21 +115,19 @@ def pull(
def load_project(
self,
project_registry_path: str,
query_param: dict | None = None,
) -> peppy.Project:
) -> peprs.Project:
"""
Load a peppy project from PEPhub as a peppy.Project object.
Load a project from PEPhub as a peprs.Project object.

Args:
project_registry_path: Registry path of the project.
query_param: Query parameters used in the get request.

Returns:
The peppy project.
The peprs project.
"""
raw_pep = self.load_raw_pep(project_registry_path, query_param)
peppy_project = peppy.Project().from_dict(raw_pep)
return peppy_project

return peprs.Project.from_pephub(project_registry_path)

def push(
self,
Expand All @@ -156,9 +150,9 @@ def push(
is_private: Specifies whether the project should be private.
force: Force push to the database. Use it to update, or upload a project.
"""
peppy_project = peppy.Project(cfg=cfg)
peprs_project = peprs.Project(cfg)
self.upload(
project=peppy_project,
project=peprs_project,
namespace=namespace,
name=name,
tag=tag,
Expand All @@ -168,15 +162,15 @@ def push(

def upload(
self,
project: peppy.Project,
project: peprs.Project,
namespace: str,
name: str = None,
tag: str = None,
is_private: bool = False,
force: bool = True,
) -> None:
"""
Upload a peppy project to PEPhub.
Upload a peprs project to PEPhub.

Args:
project: Project object that has to be uploaded to the DB.
Expand All @@ -186,17 +180,14 @@ def upload(
is_private: Make project private.
force: Overwrite project if it exists.
"""
# peprs already emits config/samples/subsamples keys, so no remapping is needed.
pep_dict = project.to_dict(
extended=True,
orient="records",
raw=True,
by_sample=True,
)
if name:
pep_dict[CONFIG_KEY][NAME_KEY] = name

pep_dict["config"] = pep_dict.pop(CONFIG_KEY)
pep_dict["samples"] = pep_dict.pop(SAMPLE_RAW_DICT_KEY)
pep_dict["subsamples"] = pep_dict.pop(SUBSAMPLE_RAW_LIST_KEY)
print(pep_dict)
upload_data = ProjectUploadData(
pep_dict=pep_dict,
tag=tag,
Expand Down Expand Up @@ -243,13 +234,13 @@ def find_project(
self,
namespace: str,
query_string: str = "",
tag: str = None,
tag: str | None = None,
limit: int = 100,
offset: int = 0,
filter_by: Literal["submission_date", "last_update_date"] = None,
start_date: str = None,
end_date: str = None,
) -> SearchReturnModel:
filter_by: Literal["submission_date", "last_update_date"] | None = None,
start_date: str | None = None,
end_date: str | None = None,
) -> SearchReturnModel | None:
"""
Find projects in a specific namespace and return a list of PEP annotations.

Expand Down Expand Up @@ -303,9 +294,9 @@ def _load_raw_pep(
registry_path: str,
jwt_data: str | None = None,
query_param: dict | None = None,
) -> dict:
) -> dict | None:
"""
Request PEPhub and return the requested project as a peppy.Project object.
Request PEPhub and return the requested project as a raw project dict.

!!! This method is deprecated. Use load_raw_pep instead. !!!

Expand All @@ -323,9 +314,9 @@ def load_raw_pep(
self,
registry_path: str,
query_param: dict | None = None,
) -> dict:
) -> dict | None:
"""
Request PEPhub and return the requested project as a peppy.Project object.
Request PEPhub and return the requested project as a raw project dict.

Args:
registry_path: Project namespace, eg. "geo/GSE124224:tag".
Expand All @@ -349,7 +340,7 @@ def load_raw_pep(
correct_proj_dict = ProjectDict(**decoded_response)

# This step is necessary because of this issue: https://github.com/pepkit/pephub/issues/124
return correct_proj_dict.model_dump(by_alias=True)
return correct_proj_dict.model_dump()

if pephub_response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError("File does not exist, or you are unauthorized.")
Expand Down Expand Up @@ -392,7 +383,7 @@ def _build_pull_request_url(self, query_param: dict = None) -> str:
return f"{self.__base_url}api/v1/projects/" + endpoint

def _build_project_search_url(
self, namespace: str, query_param: dict = None
self, namespace: str, query_param: dict | None = None
) -> str:
"""
Build the request for searching projects from pephub.
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pephubclient"
version = "0.6.1"
version = "0.7.0"
description = "PEPhub command line interface."
readme = "README.md"
license = "BSD-2-Clause"
Expand All @@ -22,10 +22,11 @@ classifiers = [
]
dependencies = [
"typer>=0.7.0",
"peppy>=0.40.5",
"peprs>=0.2.4",
"requests>=2.28.2",
"pydantic>2.5.0",
"pandas>=2.0.0",
"pyyaml>=6.0",
"ubiquerg>=0.6.3",
"coloredlogs>=15.0.1",
"toml>=0.10.2",
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def device_code_return():
def test_raw_pep_return():
sample_prj = {
"config": {
"pep_version": "2.1.0",
"This": "is config",
"description": "desc",
"name": "sample name",
Expand Down