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

Update isa json based on response #37

Merged
merged 25 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5e0431d
Add response model as a pydantic class
kdp-cloud May 28, 2024
5e7c8df
Move models to separate folder
kdp-cloud May 28, 2024
3a0683a
typo
kdp-cloud May 28, 2024
973cb32
Add test for reducing ISA JSON for BioSamples
kdp-cloud May 28, 2024
c8a52be
Modify for BioSamples
kdp-cloud May 28, 2024
7973c8c
typo
kdp-cloud May 28, 2024
8795a2e
First version of `submit_to_biosamples`
kdp-cloud May 28, 2024
d6f11f4
Add class methods to create a RepositoryResponse from JSON
kdp-cloud Jun 2, 2024
04a70e1
Add example repo responses
kdp-cloud Jun 2, 2024
53a3571
Get rid of deprecation warnings
kdp-cloud Jun 2, 2024
4623037
Add field validator to check for duplicate keys
kdp-cloud Jun 3, 2024
373ff06
Add functionality that updates the ISA-JSON
kdp-cloud Jun 4, 2024
6b8472a
Add test for add accessions to an ISA-JSON that has no accession char…
kdp-cloud Jun 4, 2024
687a70c
Ignore __init__.py
kdp-cloud Jun 4, 2024
144ccb6
Add functionality if Accession characteristics categories are present
kdp-cloud Jun 5, 2024
4851fb7
Update accession characteristic if the value is of type OntologyAnnot…
kdp-cloud Jun 5, 2024
2d3c14c
Implementing mypy type checking and conforming the current code to mypy
kdp-cloud Jun 5, 2024
306feba
Linting
kdp-cloud Jun 5, 2024
3820640
Add type checking to workflow
kdp-cloud Jun 5, 2024
f62c90b
whoops!
kdp-cloud Jun 5, 2024
dc68dbf
pass flag to install the Library stubs
kdp-cloud Jun 5, 2024
30e698b
Pass non-interactive flag
kdp-cloud Jun 5, 2024
2c3199b
stricter type annotations
kdp-cloud Jun 6, 2024
546ecd5
Make the name optional
kdp-cloud Jun 21, 2024
dac38e3
Convert OS env variable to str
kdp-cloud Jun 21, 2024
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
4 changes: 4 additions & 0 deletions .github/workflows/test-mars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ jobs:
- name: Linting
run: ruff check mars_lib/
working-directory: ${{ env.working-directory }}

- name: Type checking
run: mypy --install-types --non-interactive mars_lib/
working-directory: ${{ env.working-directory }}
2 changes: 1 addition & 1 deletion mars-cli/.coveragerc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[run]
omit = mars_lib/__init__.py, mars_lib/submit.py, mars_lib/credential.py
omit = mars_lib/__init__.py, mars_lib/submit.py, mars_lib/credential.py, mars_lib/models/__init__.py
2 changes: 1 addition & 1 deletion mars-cli/mars_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pathlib
from configparser import ConfigParser
from mars_lib.target_repo import TargetRepository
from mars_lib.model import Investigation, IsaJson
from mars_lib.models.isa_json import Investigation, IsaJson
from mars_lib.isa_json import load_isa_json
from logging.handlers import RotatingFileHandler
import requests
Expand Down
13 changes: 7 additions & 6 deletions mars-cli/mars_lib/authentication.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
from typing import Optional
import requests
import json


def get_webin_auth_token(
credentials_dict,
header={"Content-Type": "application/json"},
auth_base_url="https://wwwdev.ebi.ac.uk/ena/dev/submit/webin/auth/token",
token_expiration_time=1,
):
credentials_dict: dict[str, str],
header: dict[str, str] = {"Content-Type": "application/json"},
auth_base_url: str = "https://wwwdev.ebi.ac.uk/ena/dev/submit/webin/auth/token",
token_expiration_time: int = 1,
) -> Optional[str]:
"""
Obtain Webin authentication token.

Args:
credentials_dict (dict): The password dictionary for authentication.
header (dict): The header information.
auth_base_url (str): The base URL for authentication.
token_expiration_time(int): Toke expiration time in hours.
token_expiration_time(int): Token expiration time in hours.

Returns:
str: The obtained token.
Expand Down
67 changes: 37 additions & 30 deletions mars-cli/mars_lib/biosamples_external_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
from jsonschema import validate
from jsonschema.exceptions import ValidationError, SchemaError
from typing import Union
from typing import Union, Any, Optional, List

# -- #
# Hardcoded values
Expand All @@ -21,7 +21,7 @@
# -- #
# Code blocks
# -- #
def load_json_file(file):
def load_json_file(file: str) -> Any:
"""
Function to load a JSON file as a dictionary.
Args:
Expand All @@ -46,7 +46,7 @@ def load_json_file(file):
)


def handle_input_dict(input):
def handle_input_dict(input: dict[str, str]) -> Optional[dict[str, str]]:
"""
Function to handle the input: assert that it's either a dictionary or
the filepath to an existing file containing the dictionary
Expand All @@ -73,7 +73,7 @@ def handle_input_dict(input):
raise ValueError(f"The file '{input}' is not a valid JSON file.")


def get_header(token):
def get_header(token: str) -> dict[str, str]:
"""
Obtain the header using a token.

Expand All @@ -90,7 +90,7 @@ def get_header(token):
}


def validate_bs_accession(accession_str):
def validate_bs_accession(accession_str: str) -> None:
"""
Validates that the given accession string conforms to the specified regex format.
See: https://registry.identifiers.org/registry/biosample
Expand All @@ -108,8 +108,8 @@ def validate_bs_accession(accession_str):


def validate_json_against_schema(
json_doc: Union[dict, str], json_schema: Union[dict, str]
):
json_doc: Union[dict[str, List[str]], str], json_schema: Union[dict[str, str], str]
) -> Optional[bool]:
"""
Validates a JSON document against a given JSON Schema.

Expand Down Expand Up @@ -150,7 +150,7 @@ class BiosamplesRecord:
production: boolean indicating environment mode
"""

def __init__(self, bs_accession):
def __init__(self, bs_accession: str) -> None:
"""
Initialize the BiosamplesRecord with provided arguments.

Expand All @@ -159,16 +159,19 @@ def __init__(self, bs_accession):
"""
validate_bs_accession(bs_accession)
self.bs_accession = bs_accession
self.biosamples_credentials: Optional[dict[str, str]] = None
self.biosamples_externalReferences: List[str] = []
self.production: bool = False

def display(self):
def display(self) -> None:
"""
Display the attributes for demonstration purposes.
"""
print("Biosamples Credentials:", self.biosamples_credentials)
print("Biosamples External References:", self.biosamples_externalReferences)
print("Production Mode:", self.production)

def fetch_bs_json(self, biosamples_endpoint):
def fetch_bs_json(self, biosamples_endpoint: str) -> Optional[dict[str, str]]:
"""
Fetches the BioSample's record (JSON) of the accession.

Expand Down Expand Up @@ -206,47 +209,49 @@ def fetch_bs_json(self, biosamples_endpoint):
self.bs_json = response_json
return self.bs_json

def load_bs_json(self, bs_json_file: str = None, bs_json: dict = None):
def load_bs_json(
self, bs_json: Union[str, dict[str, str]]
) -> Optional[dict[str, str]]:
"""
Loads a given JSON, or the file containing it, as the BioSample's record (JSON) for this instance.
It is an alternative to fetching it directly from BioSample.

Args:
bs_json_file (str): The file containing the Biosamples JSON metadata of the accession
bs_json (dict): The already loaded Biosamples JSON metadata of the accession
bs_json Union[str, dict]: The already Biosamples JSON metadata of the accession either path to file or dictionary.
"""
if bs_json:
if isinstance(bs_json, dict):
self.bs_json = bs_json
return self.bs_json
else:
raise TypeError(
f"Given 'bs_json' is of type '{type(bs_json)}' instead of type 'dict'."
)
elif bs_json_file:
bs_json = load_json_file(bs_json_file)
if isinstance(bs_json, dict):
self.bs_json = bs_json
return self.bs_json
elif isinstance(bs_json, str):
bs_json_data = load_json_file(bs_json)
self.bs_json = bs_json_data
return self.bs_json
else:
raise ValueError(
"Neither the file containing the Biosamples JSON nor the Biosamples JSON itself were given to load it into the instance."
)

def pop_links(self):
def pop_links(self) -> dict[str, str]:
"""
Removes "_links" array (which is added automatically after updating the biosamples on the BioSample's side).
"""

if "_links" not in self.bs_json:
return self.bs_json
if "_links" in self.bs_json:
self.bs_json.pop("_links")

self.bs_json.pop("_links")
return self.bs_json

def extend_externalReferences(self, new_ext_refs_list):
def extend_externalReferences(
self, new_ext_refs_list: List[dict[str, str]]
) -> dict[str, str]:
"""Extends the JSON of the BioSample's record with new externalReferences"""
if not self.bs_json:
self.fetch_bs_json()
endpoint = (
biosamples_endpoints["prod"]
if self.production
else biosamples_endpoints["dev"]
)
self.fetch_bs_json(endpoint)
self.pop_links()

if "externalReferences" not in self.bs_json:
Expand All @@ -265,7 +270,9 @@ def extend_externalReferences(self, new_ext_refs_list):
self.bs_json["externalReferences"] = ext_refs_list
return self.bs_json

def update_remote_record(self, header, webin_auth="?authProvider=WEBIN"):
def update_remote_record(
self, header: dict[str, str], webin_auth: str = "?authProvider=WEBIN"
) -> Optional[str]:
"""
Updates the remote record of the BioSample's accession with the current sample JSON.

Expand Down
23 changes: 15 additions & 8 deletions mars-cli/mars_lib/credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,27 +52,31 @@


class CredentialManager:
def __init__(self, service_name):
def __init__(self, service_name: str) -> None:
self.service_name = service_name

def get_credential_env(self, username):
def get_credential_env(self, username: str) -> str:
"""
Retrieves a credential from environment variables.

:param username: The environment variable username.
:return: The value of the environment variable or None if not found.
"""
return os.getenv(username)
result = os.getenv(username)
if result is None:
raise ValueError(f"Environment variable '{username}' not found.")

def prompt_for_password(self):
return result

def prompt_for_password(self) -> str:
"""
Securely prompts the user to enter a password in the console.

:return: The password entered by the user.
"""
return getpass.getpass(prompt="Enter your password: ")

def set_password_keyring(self, username, password):
def set_password_keyring(self, username: str, password: str) -> None:
"""
Stores a password in the keyring under the given username.

Expand All @@ -81,16 +85,19 @@ def set_password_keyring(self, username, password):
"""
keyring.set_password(self.service_name, username, password)

def get_password_keyring(self, username):
def get_password_keyring(self, username: str) -> str:
"""
Retrieves a password from the keyring for the given username.

:param username: The username whose password to retrieve.
:return: The password or None if not found.
"""
return keyring.get_password(self.service_name, username)
pwd = keyring.get_password(self.service_name, username)
if pwd is None:
raise ValueError(f"Password not found for username '{username}'.")
return pwd

def delete_password_keyring(self, username):
def delete_password_keyring(self, username: str) -> None:
"""
Deletes a password from the keyring for the given username.

Expand Down
Loading