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

DISCO-1604: Protocol support, salt class, metalot structure fix and logging #167

Merged
merged 8 commits into from
Oct 25, 2024
57 changes: 54 additions & 3 deletions acasclient/CmpdReg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,57 @@
import types
import logging
from .acasclient import client
from .lsthing import convert_json, camel_to_underscore

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)



class Salt():
"""Salt class."""
def __init__(self, abbrev: str, name: str, mol_structure: str, id: int = None, **kwargs):
self.id = id
self.abbrev = abbrev
self.name = name
self.mol_structure = mol_structure

def save(self, client: client) -> Salt:
"""Save the salt to the server."""
resp = client.create_salt(self.abbrev, self.name, self.mol_structure)
self.id = resp['id']
return self

@classmethod
def from_camel_dict(cls, data):
brianbolt marked this conversation as resolved.
Show resolved Hide resolved
"""Construct an AbstractModel object from a camelCase dict

:param data: dict of attributes
:type data: dict
:return: instance of class AbstractModel
:rtype: AbstractModel
"""
snake_case_dict = convert_json(data, camel_to_underscore)
return cls.from_dict(snake_case_dict)

@classmethod
def from_dict(cls, data):
"""Construct an AbstractModel object from a dict

:param data: dict of attributes
:type data: dict
:return: instance of class AbstractModel
:rtype: AbstractModel
"""
return cls(**data)

def as_dict(self) -> dict:
return {
'id': self.id,
'abbrev': self.abbrev,
'name': self.name,
'mol_structure': self.mol_structure
}

class AdditionalScientistType(Enum):
"""Enum for additional scientist types."""
COMPOUND = ACASDDict('compound', 'scientist')
Expand Down Expand Up @@ -62,7 +108,7 @@ def meta_lots_to_dict_array(meta_lots: list[dict]) -> list[dict]:
return [meta_lot_to_dict(meta_lot) for meta_lot in meta_lots]


def meta_lot_to_dict(meta_lot: dict) -> dict:
def meta_lot_to_dict(meta_lot: dict, use_parent_mol=True) -> dict:
"""Converts a meta lot to a dictionary into a flat dictionary of specific fields."""
parent_common_names = [parent_alias["aliasName"] for parent_alias in meta_lot["lot"]["saltForm"]["parent"]["parentAliases"] if parent_alias["lsKind"] == "Common Name"]

Expand All @@ -80,8 +126,13 @@ def meta_lot_to_dict(meta_lot: dict) -> dict:
# If lot amount is filled in, then we need to fill in lot barcode with the lot corp name
lot_barcode = meta_lot["lot"]["corpName"] if meta_lot["lot"]["amount"] is not None else None

if use_parent_mol:
mol = meta_lot["lot"]["parent"]["molStructure"]
bffrost marked this conversation as resolved.
Show resolved Hide resolved
else:
mol = meta_lot["lot"]['asDrawnStruct'] if meta_lot["lot"]['asDrawnStruct'] is not None else meta_lot["lot"]["parent"]["molStructure"]

return_dict = {
'mol': meta_lot["lot"]['asDrawnStruct'] if meta_lot["lot"]['asDrawnStruct'] is not None else meta_lot["lot"]["parent"]["molStructure"],
'mol': mol,
"id": meta_lot["lot"]["id"],
"name": meta_lot["lot"]["corpName"],
"parent_common_name": '; '.join(parent_common_names) if len(parent_common_names) > 0 else None,
Expand Down
92 changes: 92 additions & 0 deletions acasclient/acasclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,7 +960,99 @@ def get_protocols_by_label(self, label):
.format(self.url, label))
resp.raise_for_status()
return resp.json()

def get_protocol_by_code(self, protocol_code):
"""Get a protocol from a protocol code

Get a protocol given a protocol code

Args:
protocol_code (str): A protocol code

Returns: Returns a protocol object
"""
resp = self.session.get("{}/api/protocols/codename/{}"
.format(self.url, protocol_code))
if resp.status_code == 500:
return None
resp.raise_for_status()
return resp.json()

def get_all_protocol_stubs(self):
"""Get all protocol stubs

Get an array of all protocols

Args:
None

list of dict: Returns an array of protocol stubs in the format:
[
{
'id': int,
'code': str,
'name': str,
'ignored': str
},
...
]
"""
resp = self.session.get("{}/api/protocolCodes"
bffrost marked this conversation as resolved.
Show resolved Hide resolved
.format(self.url))
resp.raise_for_status()
protocols = resp.json()
# Remove any dupes using the id (BUG in ACAS)
return [dict(t) for t in {tuple(d.items()) for d in protocols}]

def save_protocol(self, protocol):
"""Save a protocol

Save a protocol to ACAS

Args:
protocol (dict): A protocol object

Returns: Returns a protocol object
"""
if protocol.get("id") and protocol.get("codeName"):
resp_dict = self.update_protocol(protocol)
else:
resp_dict = self.create_protocol(protocol)
return resp_dict

def update_protocol(self, protocol):
"""Update a protocol

Update a protocol in ACAS

Args:
protocol (dict): A protocol object

Returns: Returns a protocol object
"""
resp = self.session.put("{}/api/protocols/{}".format(self.url, protocol.get('id')),
headers={'Content-Type': "application/json"},
data=json.dumps(protocol))
resp.raise_for_status()
return resp.json()

def create_protocol(self, protocol):
"""Create a protocol

Create a protocol in ACAS

Args:
protocol (dict): A protocol object

Returns: Returns a protocol object
"""
resp = self.session.post("{}/api/protocols".format(self.url),
headers={'Content-Type': "application/json"},
data=json.dumps(protocol))
resp.raise_for_status()
return resp.json()


def get_experiments_by_protocol_code(self, protocol_code):
"""Get all experiments for a protocol from a protocol code

Expand Down
19 changes: 17 additions & 2 deletions acasclient/ddict.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import logging

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


class DDict(object):
"""The DDict class is meant as a generic interface for any implementation of a Data Dictionary that
Expand Down Expand Up @@ -71,3 +69,20 @@ def update_valid_values(self, client):
self.valid_values = [val_dict['code'] for val_dict in valid_codetables]
if not self.valid_values:
self.raise_empty_dict_error()

class ACASAuthorDDict(DDict):
"""DDict implementation for built-in ACAS DDicts """

CODE_ORIGIN = 'ACAS authors'

def __init__(self, code_type, code_kind):
super(ACASAuthorDDict, self).__init__(code_type, code_kind, self.CODE_ORIGIN)

def update_valid_values(self, client):
"""Get the valid values for the DDict."""
valid_authors = client.get_authors()
# Raise error if author does not match name
self.valid_values = [val_dict['code'] for val_dict in valid_authors]
if not self.valid_values:
self.raise_empty_dict_error()

2 changes: 1 addition & 1 deletion acasclient/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,4 @@ def scientist(self) -> str:
value_kind="scientist")
return data.get('codeValue') if data else None

as_dict = dict.__str__
as_dict = dict.__str__
31 changes: 28 additions & 3 deletions acasclient/lsthing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from typing import Any, Dict

from .ddict import ACASDDict, ACASLsThingDDict, DDict
from .ddict import ACASDDict, ACASLsThingDDict, DDict, ACASAuthorDDict
from .interactions import INTERACTION_VERBS_DICT, opposite
from .validation import validation_result, ValidationResult

Expand All @@ -18,12 +18,11 @@
from six import text_type as str

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)


ROW_NUM_KEY = 'row number'
ACAS_DDICT = ACASDDict.CODE_ORIGIN.upper()
ACAS_LSTHING = ACASLsThingDDict.CODE_ORIGIN.upper()
ACAS_AUTHOR = ACASAuthorDDict.CODE_ORIGIN.upper()

# JSON encoding / decoding

Expand Down Expand Up @@ -850,7 +849,31 @@ def as_camel_dict(self):
snake_case_dict = self.as_dict()
camel_dict = convert_json(snake_case_dict, underscore_to_camel)
return camel_dict

def __deepcopy__(self, memo):
"""Create a deep copy of the instance.

The 'id' and 'version' fields are nulled out to ensure that the copied object
is treated as a new instance rather than a duplicate of the original. This is
particularly useful when the copied object needs to be saved as a new record
in a database or used in a context where unique identifiers are required.

Args:
memo (dict): A dictionary of objects already copied during the current copying pass.

Returns:
object: A deep copy of the instance with 'id' and 'version' fields set to None.
"""
# Create a deep copy of the instance first
copied_obj = copy.copy(self) # Shallow copy of self
# Then deep copy the attributes individually
copied_obj.__dict__ = copy.deepcopy(self.__dict__, memo)
# Set the 'id' attribute to None
copied_obj.id = None
brianbolt marked this conversation as resolved.
Show resolved Hide resolved
copied_obj.version = None
return copied_obj


def as_json(self, **kwargs):
"""Serialize instance into a JSON string with camelCase keys

Expand Down Expand Up @@ -2136,6 +2159,8 @@ def _get_ddicts(self):
ddict = ACASDDict(value.code_type, value.code_kind)
elif value.code_origin.upper() == ACAS_LSTHING:
ddict = ACASLsThingDDict(value.code_type, value.code_kind)
elif value.code_origin.upper() == ACAS_AUTHOR:
ddict = ACASAuthorDDict(value.code_type, value.code_kind)
else:
raise ValueError(f'Unsupported code_origin: {value.code_origin}')
ddicts[(ddict.code_type, ddict.code_kind, ddict.code_origin.upper())] = ddict
Expand Down
69 changes: 69 additions & 0 deletions acasclient/protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from acasclient.lsthing import (CodeValue, SimpleLsThing, LsThing, clob, ACAS_DDICT, ACAS_AUTHOR)

from datetime import datetime

# Constants
META_DATA = 'protocol metadata'
LS_TYPE = 'default'
LS_KIND = 'default'
PREFERRED_LABEL = 'protocol name'
STATUS = 'protocol status'
NOTEBOOK_PAGE = 'notebook page'
PROJECT = 'project'
COMMENTS = 'comments'
ASSAY_TREE_RULE = 'assay tree rule'
PROTOCOL_STATUS = 'protocol status'
ASSAY_STAGE = 'assay stage'
SCIENTIST = 'scientist'
ASSAY_PRINCIPLE = 'assay principle'
CREATION_DATE = 'creation date'
PROTOCOL_DETAILS = 'protocol details'
NOTEBOOK = 'notebook'


class Protocol(SimpleLsThing):
brianbolt marked this conversation as resolved.
Show resolved Hide resolved
ls_type = LS_TYPE
ls_kind = LS_KIND
preferred_label_kind = PREFERRED_LABEL

def __init__(self, name=None, scientist=None, recorded_by=None, assay_principle=None, assay_stage='unassigned', creation_date=None, protocol_details=None,
notebook=None, comments=None, assay_tree_rule=None, protocol_status='created', notebook_page=None, project='unassigned', ls_thing=None):
names = {PREFERRED_LABEL: name}

if not creation_date:
creation_date = datetime.now()

metadata = {
META_DATA: {
ASSAY_PRINCIPLE: clob(assay_principle),
ASSAY_STAGE: CodeValue(assay_stage, 'assay', 'stage', ACAS_DDICT),
SCIENTIST: CodeValue(scientist, 'assay', 'scientist', ACAS_AUTHOR),
CREATION_DATE: creation_date,
PROTOCOL_DETAILS: clob(protocol_details),
NOTEBOOK: notebook,
COMMENTS: clob(comments),
ASSAY_TREE_RULE: assay_tree_rule,
PROTOCOL_STATUS: CodeValue(protocol_status, 'protocol', 'status', ACAS_DDICT),
NOTEBOOK_PAGE: notebook_page,
PROJECT: CodeValue(project, 'project', 'biology', ACAS_DDICT)
}
}
super(Protocol, self).__init__(ls_type=self.ls_type, ls_kind=self.ls_kind, names=names, recorded_by=recorded_by,
preferred_label_kind=self.preferred_label_kind, metadata=metadata, ls_thing=ls_thing)

def save(self, client, skip_validation=False):
"""Persist changes to the ACAS server.

:param client: Authenticated instances of acasclient.client
:type client: acasclient.client
"""
# Run validation
if not skip_validation:
self.validate(client)
self._prepare_for_save(client)
# Persist
protocol_dict = self._ls_thing.as_camel_dict()
resp_dict = client.save_protocol(protocol_dict)
self._ls_thing = LsThing.from_camel_dict(resp_dict)
brianbolt marked this conversation as resolved.
Show resolved Hide resolved
self._cleanup_after_save()
return self
5 changes: 4 additions & 1 deletion acasclient/selfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,11 @@
################################################################################
# Logging
################################################################################
logger = logging.getLogger(__name__).addHandler(logging.NullHandler())
# Initialize the logger
logger = logging.getLogger(__name__)

# Add a NullHandler to avoid errors if no other handlers are configured
logger.addHandler(logging.NullHandler())

################################################################################
# Functions
Expand Down
Loading
Loading