Skip to content

Commit

Permalink
Merge pull request #167 from mcneilco/DISCO-1604
Browse files Browse the repository at this point in the history
DISCO-1604: Protocol support, salt class, metalot structure fix and logging
  • Loading branch information
brianbolt authored Oct 25, 2024
2 parents fdfd1de + 692d1bb commit de0d461
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 13 deletions.
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):
"""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"]
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, quote(label, safe='')))
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"
.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 @@ -288,4 +288,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
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):
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)
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

0 comments on commit de0d461

Please sign in to comment.