Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Samples and views API #36

Merged
merged 11 commits into from
Feb 12, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/pytest-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.10"]
python-version: ["3.11"]
os: [windows-latest]

steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: ["3.8", "3.11"]
python-version: ["3.8", "3.12"]
os: [ubuntu-20.04]

steps:
Expand Down
3 changes: 2 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
include requirements/*
include README.md
include pephubclient/pephub_oauth/*
include pephubclient/pephub_oauth/*
include pephubclient/modules/*
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ run-coverage:
coverage run -m pytest

html-report:
coverage html
coverage html --omit="*/test*"

open-coverage:
cd htmlcov && google-chrome index.html
Expand Down
10 changes: 10 additions & 0 deletions pephubclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pephubclient.pephubclient import PEPHubClient
from pephubclient.helpers import is_registry_path, save_pep
import logging
import coloredlogs

__app_name__ = "pephubclient"
__version__ = "0.3.0"
Expand All @@ -14,3 +16,11 @@
"is_registry_path",
"save_pep",
]


_LOGGER = logging.getLogger(__app_name__)
coloredlogs.install(
logger=_LOGGER,
datefmt="%H:%M:%S",
fmt="[%(levelname)s] [%(asctime)s] %(message)s",
)
13 changes: 13 additions & 0 deletions pephubclient/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
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 = (
f"{PEPHUB_BASE_URL}api/v1/projects/{{namespace}}/{{project}}/views/{{view_name}}"
)
PEPHUB_VIEW_SAMPLE_URL = f"{PEPHUB_BASE_URL}api/v1/projects/{{namespace}}/{{project}}/views/{{view_name}}/{{sample_name}}"

khoroshevskyi marked this conversation as resolved.
Show resolved Hide resolved

class RegistryPath(BaseModel):
protocol: Optional[str] = None
Expand All @@ -33,3 +39,10 @@ class ResponseStatusCodes(int, Enum):
NOT_EXIST = 404
CONFLICT = 409
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
khoroshevskyi marked this conversation as resolved.
Show resolved Hide resolved
1 change: 0 additions & 1 deletion pephubclient/files_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import yaml
import zipfile

from pephubclient.constants import RegistryPath
from pephubclient.exceptions import PEPExistsError


Expand Down
35 changes: 33 additions & 2 deletions pephubclient/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import requests
from requests.exceptions import ConnectionError
from urllib.parse import urlencode

from ubiquerg import parse_registry_path
from pydantic import ValidationError
Expand Down Expand Up @@ -47,20 +48,50 @@ def send_request(
)

@staticmethod
def decode_response(response: requests.Response, encoding: str = "utf-8") -> str:
def decode_response(
response: requests.Response, encoding: str = "utf-8", output_json: bool = False
) -> Union[str, dict]:
"""
Decode the response from GitHub and pack the returned data into appropriate model.

:param response: Response from GitHub.
:param encoding: Response encoding [Default: utf-8]
:param output_json: If True, return response in json format
:return: Response data as an instance of correct model.
"""

try:
return response.content.decode(encoding)
if output_json:
return response.json()
else:
return response.content.decode(encoding)
except json.JSONDecodeError as err:
raise ResponseError(f"Error in response encoding format: {err}")

@staticmethod
def parse_query_param(pep_variables: dict) -> str:
"""
Grab all the variables passed by user (if any) and parse them to match the format specified
by PEPhub API for query parameters.

:param pep_variables: dict of query parameters
:return: PEPHubClient variables transformed into string in correct format.
"""
return "?" + urlencode(pep_variables)

@staticmethod
def parse_header(jwt_data: Optional[str] = None) -> dict:
"""
Create Authorization header

:param jwt_data: jwt string
:return: Authorization dict
"""
if jwt_data:
return {"Authorization": jwt_data}
else:
return {}


class MessageHandler:
"""
Expand Down
5 changes: 4 additions & 1 deletion pephubclient/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import datetime
from typing import Optional, List
from typing import Optional, List, Union

from pydantic import BaseModel, Field, field_validator, ConfigDict
from peppy.const import CONFIG_KEY, SUBSAMPLE_RAW_LIST_KEY, SAMPLE_RAW_DICT_KEY
Expand Down Expand Up @@ -43,6 +43,9 @@ class ProjectAnnotationModel(BaseModel):
submission_date: datetime.datetime
digest: str
pep_schema: str
pop: bool = False
stars_number: Optional[int] = 0
forked_from: Optional[Union[str, None]] = None


class SearchReturnModel(BaseModel):
Expand Down
File renamed without changes.
208 changes: 208 additions & 0 deletions pephubclient/modules/sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import logging

from pephubclient.helpers import RequestManager
from pephubclient.constants import PEPHUB_SAMPLE_URL, ResponseStatusCodes
from pephubclient.exceptions import ResponseError

_LOGGER = logging.getLogger("pephubclient")


class PEPHubSample(RequestManager):
"""
Class for managing samples in PEPhub and provides methods for
getting, creating, updating and removing samples.
This class is not related to peppy.Sample class.
"""

def __init__(self, jwt_data: str = None):
"""
:param jwt_data: jwt token for authorization
"""

self.__jwt_data = jwt_data

def get(
self,
namespace: str,
name: str,
tag: str,
sample_name: str = None,
) -> dict:
"""
Get sample from project in PEPhub.

:param namespace: namespace of project
:param name: name of project
:param tag: tag of project
:param sample_name: sample name
:return: Sample object
"""
url = self._build_sample_request_url(
namespace=namespace, name=name, sample_name=sample_name
)

url = url + self.parse_query_param(pep_variables={"tag": tag})

response = self.send_request(
method="GET", url=url, headers=self.parse_header(self.__jwt_data)
)
if response.status_code == ResponseStatusCodes.OK:
return self.decode_response(response, output_json=True)
if response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError(
f"Sample does not exist. Project: '{namespace}/{name}:{tag}'. Sample_name: '{sample_name}'"
)
elif response.status_code == ResponseStatusCodes.INTERNAL_ERROR:
raise ResponseError("Internal server error. Unexpected return value.")
else:
raise ResponseError(
f"Unexpected return value. Error: {response.status_code}"
)

def create(
self,
namespace: str,
name: str,
tag: str,
sample_name: str,
sample_dict: dict,
overwrite: bool = False,
) -> None:
"""
Create sample in project in PEPhub.

:param namespace: namespace of project
:param name: name of project
:param tag: tag of project
:param sample_dict: sample dict
:param sample_name: sample name
:param overwrite: overwrite sample if it exists
:return: None
"""
url = self._build_sample_request_url(
namespace=namespace,
name=name,
sample_name=sample_name,
)

url = url + self.parse_query_param(
pep_variables={"tag": tag, "overwrite": overwrite}
)

# add sample name to sample_dict if it is not there
if sample_name not in sample_dict.values():
sample_dict["sample_name"] = sample_name

response = self.send_request(
method="POST",
url=url,
headers=self.parse_header(self.__jwt_data),
khoroshevskyi marked this conversation as resolved.
Show resolved Hide resolved
json=sample_dict,
)
if response.status_code == ResponseStatusCodes.ACCEPTED:
_LOGGER.info(
f"Sample '{sample_name}' added to project '{namespace}/{name}:{tag}' successfully."
)
return None
elif response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError(f"Project '{namespace}/{name}:{tag}' does not exist.")
elif response.status_code == ResponseStatusCodes.CONFLICT:
raise ResponseError(
f"Sample '{sample_name}' already exists. Set overwrite to True to overwrite sample."
)
else:
raise ResponseError(
f"Unexpected return value. Error: {response.status_code}"
)

def update(
self,
namespace: str,
name: str,
tag: str,
sample_name: str,
sample_dict: dict,
):
"""
Update sample in project in PEPhub.

:param namespace: namespace of project
:param name: name of project
:param tag: tag of project
:param sample_name: sample name
:param sample_dict: sample dict, that contain elements to update, or
:return: None
"""

url = self._build_sample_request_url(
namespace=namespace, name=name, sample_name=sample_name
)

url = url + self.parse_query_param(pep_variables={"tag": tag})

response = self.send_request(
method="PATCH",
url=url,
headers=self.parse_header(self.__jwt_data),
json=sample_dict,
)
if response.status_code == ResponseStatusCodes.ACCEPTED:
_LOGGER.info(
f"Sample '{sample_name}' updated in project '{namespace}/{name}:{tag}' successfully."
)
return None
elif response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError(
f"Sample '{sample_name}' or project {namespace}/{name}:{tag} does not exist. Error: {response.status_code}"
)
else:
raise ResponseError(
f"Unexpected return value. Error: {response.status_code}"
)

def remove(self, namespace: str, name: str, tag: str, sample_name: str):
"""
Remove sample from project in PEPhub.

:param namespace: namespace of project
:param name: name of project
:param tag: tag of project
:param sample_name: sample name
:return: None
"""
url = self._build_sample_request_url(
namespace=namespace, name=name, sample_name=sample_name
)

url = url + self.parse_query_param(pep_variables={"tag": tag})

response = self.send_request(
method="DELETE",
url=url,
headers=self.parse_header(self.__jwt_data),
)
if response.status_code == ResponseStatusCodes.ACCEPTED:
_LOGGER.info(
f"Sample '{sample_name}' removed from project '{namespace}/{name}:{tag}' successfully."
)
return None
elif response.status_code == ResponseStatusCodes.NOT_EXIST:
raise ResponseError(
f"Sample '{sample_name}' or project {namespace}/{name}:{tag} does not exist. Error: {response.status_code}"
)
else:
raise ResponseError(
f"Unexpected return value. Error: {response.status_code}"
)

@staticmethod
def _build_sample_request_url(namespace: str, name: str, sample_name: str) -> str:
"""
Build url for sample request.

:param namespace: namespace where project will be uploaded
:return: url string
"""
return PEPHUB_SAMPLE_URL.format(
namespace=namespace, project=name, sample_name=sample_name
)
Loading
Loading