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
15 changes: 13 additions & 2 deletions peppy/pephubclient/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@

from .helpers import call_client_func
from .pephubclient import PEPHubClient
from .schemas.schema_cli import schemas_app

_client = PEPHubClient()

app = typer.Typer()


@app.command()
def login():
def login(
token: str = typer.Option(
None, help="JWT token from PEPhub. If provided, skips the browser login."
),
url: str = typer.Option(
None, help="Base URL for PEPhub, if not using the default host."
),
):
"""
Login to PEPhub
"""
call_client_func(_client.login)
call_client_func(_client.login, token=token, url=url)


@app.command()
Expand Down Expand Up @@ -71,3 +79,6 @@ def push(
is_private=is_private,
force=force,
)


app.add_typer(schemas_app, name="schema")
29 changes: 18 additions & 11 deletions peppy/pephubclient/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@

from pydantic import BaseModel, field_validator

PEPHUB_BASE_URL = os.getenv(
"PEPHUB_BASE_URL", default="https://pephub-api.databio.org/"
)
DEFAULT_BASE_URL = "https://pephub-api.databio.org/"
PEPHUB_BASE_URL = os.getenv("PEPHUB_BASE_URL", default=DEFAULT_BASE_URL)
# PEPHUB_BASE_URL = "http://0.0.0.0:8000/"
PEPHUB_PEP_API_BASE_URL = f"{PEPHUB_BASE_URL}api/v1/projects/"
PEPHUB_PEP_SEARCH_URL = f"{PEPHUB_BASE_URL}api/v1/namespaces/{{namespace}}/projects"
PEPHUB_PUSH_URL = f"{PEPHUB_BASE_URL}api/v1/namespaces/{{namespace}}/projects/json"

PEPHUB_SAMPLE_URL = f"{PEPHUB_BASE_URL}api/v1/projects/{{namespace}}/{{project}}/samples/{{sample_name}}"
PEPHUB_VIEW_URL = (
Expand All @@ -30,6 +26,13 @@ def tag_should_not_be_none(cls, v):
return v or "default"


class CachedToken(BaseModel):
"""Credentials persisted to the TOML cache file."""

token: str | None = None
base_url: str = DEFAULT_BASE_URL


class ResponseStatusCodes(int, Enum):
OK = 200
ACCEPTED = 202
Expand All @@ -40,8 +43,12 @@ class ResponseStatusCodes(int, Enum):
INTERNAL_ERROR = 500


USER_DATA_FILE_NAME = "jwt.txt"
HOME_PATH = os.getenv("HOME")
if not HOME_PATH:
HOME_PATH = os.path.expanduser("~")
PATH_TO_FILE_WITH_JWT = os.path.join(HOME_PATH, ".pephubclient/") + USER_DATA_FILE_NAME
USER_DATA_FILE_NAME = "jwt.toml"

PH_HOME = os.getenv("PH_HOME")
if PH_HOME:
_CACHE_DIR = PH_HOME
else:
HOME_PATH = os.getenv("HOME") or os.path.expanduser("~")
_CACHE_DIR = os.path.join(HOME_PATH, ".pephubclient")
PATH_TO_TOKEN_FILE = os.path.join(_CACHE_DIR, USER_DATA_FILE_NAME)
8 changes: 8 additions & 0 deletions peppy/pephubclient/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ class PEPExistsError(BasePephubclientException):
def __init__(self, message: str | None = None):
self.message = message
super().__init__(self.message or self.default_message)


class FileDoesNotExistError(BasePephubclientException):
default_message = "File does not exist."

def __init__(self, message: str | None = None):
self.message = message
super().__init__(self.message or self.default_message)
15 changes: 9 additions & 6 deletions peppy/pephubclient/files_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,28 @@
from pathlib import Path

import pandas
import toml
import yaml

from .constants import CachedToken
from .exceptions import PEPExistsError


class FilesManager:
@staticmethod
def save_jwt_data_to_file(path: str, jwt_data: str) -> None:
"""Save jwt to provided path."""
def save_token_data(path: str, token: CachedToken) -> None:
"""Save cached token data (jwt + base url) to provided path as TOML."""
Path(os.path.dirname(path)).mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
f.write(jwt_data)
toml.dump(token.model_dump(), f)

@staticmethod
def load_jwt_data_from_file(path: str) -> str:
"""Open the file with username and ID and load this data."""
def load_token_data(path: str) -> CachedToken:
"""Load cached token data from the TOML file, or return defaults if missing."""
with suppress(FileNotFoundError):
with open(path, "r") as f:
return f.read()
return CachedToken(**toml.load(f))
return CachedToken()
Comment on lines 25 to +28

@staticmethod
def create_project_folder(
Expand Down
87 changes: 85 additions & 2 deletions peppy/pephubclient/helpers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import json
import os
from collections.abc import Callable
from typing import Any
from pathlib import Path
from typing import Any, Literal
from urllib.parse import urlencode

import pandas as pd
Expand All @@ -22,7 +23,7 @@
)
from ..project import Project
from .constants import RegistryPath
from .exceptions import PEPExistsError, ResponseError
from .exceptions import BasePephubclientException, PEPExistsError, ResponseError
from .files_manager import FilesManager
from .models import ProjectDict

Expand Down Expand Up @@ -345,3 +346,85 @@ def save_pep(
parent_path=project_path, folder_name=file_name
)
_save_unzipped_pep(project, folder_path, force=force)


def open_schema(file_path: str | Path) -> dict:
"""
Open schema file that is saved in yaml or json format.

Args:
file_path: path to the schema file

Raises:
FileNotFoundError: if file doesn't exist

Returns:
file object in dict format.
"""
if isinstance(file_path, str):
file_path = Path(file_path)

if not file_path.is_file():
raise FileNotFoundError(
f"Provided schema file doesn't exist. File path: `{str(file_path)}`"
)

if file_path.suffix == ".yaml" or file_path.suffix == ".yml":
with open(file_path, "r") as file:
data = yaml.safe_load(file)

elif file_path.suffix == ".json":
with open(file_path, "r") as file:
data = json.load(file)
else:
raise BasePephubclientException(
f"Incorrect file format provided: '{file_path.suffix}'. "
"Only yaml and json formats are supported."
)

return data


def save_schema(
file_path: str | Path,
schema_obj: dict,
format: Literal["json", "yaml"] = "yaml",
) -> None:
"""
Save dict object as file in json or yaml format.

Args:
file_path: path to the file
schema_obj: content to be saved in the file
format: Format in which file should be saved on disc. Default: yaml
"""
if format == "yaml":
schema_obj = yaml.dump(schema_obj)

elif format == "json":
schema_obj = json.dumps(schema_obj, indent=4)
else:
raise BasePephubclientException(f"Incorrect format provided: '{format}'")

with open(file_path, "w") as file:
file.write(schema_obj)


def schema_path_converter(schema_path: str) -> tuple[str, str, str]:
"""
Convert schema path to namespace, name, version.

Args:
schema_path: schema path that has structure: "namespace/name.yaml"

Returns:
tuple(namespace, name, version).
"""
if "/" in schema_path:
namespace, name_tag = schema_path.split("/")
if ":" in name_tag:
name, version = name_tag.split(":")
return namespace, name, version

return namespace, name_tag, "latest"
raise BasePephubclientException(f"Error in: '{schema_path}'")
4 changes: 2 additions & 2 deletions peppy/pephubclient/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class ProjectDict(BaseModel):
"""

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

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

Expand Down
6 changes: 2 additions & 4 deletions peppy/pephubclient/pephub_oauth/const.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# constants of pephub_auth

from ..constants import PEPHUB_BASE_URL

PEPHUB_DEVICE_INIT_URI = f"{PEPHUB_BASE_URL}auth/device/init"
PEPHUB_DEVICE_TOKEN_URI = f"{PEPHUB_BASE_URL}auth/device/token"
DEVICE_INIT_PATH = "auth/device/init"
DEVICE_TOKEN_PATH = "auth/device/token"
11 changes: 7 additions & 4 deletions peppy/pephubclient/pephub_oauth/pephub_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import requests
from pydantic import BaseModel

from ..constants import DEFAULT_BASE_URL
from ..helpers import MessageHandler, RequestManager
from ..pephub_oauth.const import PEPHUB_DEVICE_INIT_URI, PEPHUB_DEVICE_TOKEN_URI
from ..pephub_oauth.const import DEVICE_INIT_PATH, DEVICE_TOKEN_PATH
from ..pephub_oauth.exceptions import (
PEPHubResponseException,
PEPHubTokenExchangeException,
Expand All @@ -19,7 +20,9 @@
class PEPHubAuth(RequestManager):
"""Class responsible for authorization to PEPhub."""

def login_to_pephub(self):
def login_to_pephub(self, base_url: str | None = None):
self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/") + "/"

pephub_response = self._request_pephub_for_device_code()
print(
f"User verification code: {pephub_response.device_code}, please go to the website: "
Expand Down Expand Up @@ -57,7 +60,7 @@ def _request_pephub_for_device_code(self) -> InitializeDeviceCodeResponse:
"""Requests device code from pephub."""
response = PEPHubAuth.send_request(
method="POST",
url=PEPHUB_DEVICE_INIT_URI,
url=f"{self._base_url}{DEVICE_INIT_PATH}",
params=None,
headers=None,
)
Expand All @@ -72,7 +75,7 @@ def _exchange_device_code_on_token(self, device_code: str) -> str:
"""
response = PEPHubAuth.send_request(
method="POST",
url=PEPHUB_DEVICE_TOKEN_URI,
url=f"{self._base_url}{DEVICE_TOKEN_PATH}",
params=None,
headers={"device-code": device_code},
)
Expand Down
Loading