Skip to content

Commit

Permalink
Add variants related models
Browse files Browse the repository at this point in the history
  • Loading branch information
gregorjerse committed Feb 11, 2025
1 parent 74ba4ec commit 7c23807
Show file tree
Hide file tree
Showing 8 changed files with 363 additions and 36 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Changed
Added
-----
- Add ``restart`` method to the ``Data`` resource
- Add variants related models

Fixed
-----
Expand Down
43 changes: 41 additions & 2 deletions src/resdk/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,35 @@ def _clone(self):

def _dehydrate_resources(self, obj):
"""Iterate through object and replace all objects with their ids."""
print("Dehydrating", obj, type(obj))
if isinstance(obj, BaseResource):
print("Base")
return obj.id
if isinstance(obj, dict):
print("Dict")
return {key: self._dehydrate_resources(value) for key, value in obj.items()}
if self._non_string_iterable(obj):
print("Non string iterable")
return [self._dehydrate_resources(element) for element in obj]

print("Returning unchanged", obj, type(obj))
return obj

def _add_filter(self, filter_):
"""Add filtering parameters."""
print("Filter for ", self.resource)
for key, value in filter_.items():
# 'sample' is called 'entity' in the backend.
key = key.replace("sample", "entity")
if not self.resource.__name__.startswith("Variant"):
print("Replacing sample with entity in", key)
key = key.replace("sample", "entity")
print("Adding filter", key, value)
value = self._dehydrate_resources(value)
print("Dehidrated value", value, type(value))
if self._non_string_iterable(value):
print("Iterable")
value = ",".join(map(str, value))
if self.resource.query_method == "GET":
print("Appending value", value)
self._filters[key].append(value)
elif self.resource.query_method == "POST":
self._filters[key] = value
Expand Down Expand Up @@ -211,6 +222,8 @@ def _fetch(self):

filters = self._compose_filters()
if self.resource.query_method == "GET":
print("Query with filters", filters)
print("My api", self.api)
items = self.api.get(**filters)
elif self.resource.query_method == "POST":
items = self.api.post(filters)
Expand Down Expand Up @@ -285,6 +298,8 @@ def get(self, *args, **kwargs):
kwargs["limit"] = kwargs.get("limit", 1)

new_query = self._clone()

print("Adding filters", kwargs)
new_query._add_filter(kwargs)

response = list(new_query)
Expand Down Expand Up @@ -400,6 +415,30 @@ def from_path(self, full_path: str) -> "AnnotationField":
return self.get(name=field_name, group__name=group_name)


class VariantCallQuery(ResolweQuery):
"""Do not translate 'sample' to 'entity'."""

def _add_filter(self, filter_):
"""Add filtering parameters."""
for key, value in filter_.items():
# 'sample' is called 'entity' in the backend.
print("Adding filter", key, value)
value = self._dehydrate_resources(value)
print("Dehidrated value", value, type(value))
if self._non_string_iterable(value):
print("Iterable")
value = ",".join(map(str, value))
if self.resource.query_method == "GET":
print("Appending value", value)
self._filters[key].append(value)
elif self.resource.query_method == "POST":
self._filters[key] = value
else:
raise NotImplementedError(
"Unsupported query_method: {}".format(self.resource.query_method)
)


class AnnotationValueQuery(ResolweQuery):
"""Populate Annotation fields with a single query."""

Expand Down
9 changes: 9 additions & 0 deletions src/resdk/resolwe.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
Relation,
Sample,
User,
Variant,
VariantAnnotation,
VariantCall,
VariantExperiment,
)
from .resources.base import BaseResource
from .resources.kb import Feature, Mapping
Expand Down Expand Up @@ -114,6 +118,10 @@ class Resolwe:
resource_query_mapping = {
AnnotationField: "annotation_field",
AnnotationValue: "annotation_value",
Variant: "variant",
VariantAnnotation: "variant_annotation",
VariantExperiment: "variant_experiment",
VariantCall: "variant_calls",
Data: "data",
Collection: "collection",
Sample: "sample",
Expand All @@ -126,6 +134,7 @@ class Resolwe:
Mapping: "mapping",
Geneset: "geneset",
Metadata: "metadata",
Variant: "variant",
}
# Map ResolweQuery name to it's slug_field
slug_field_mapping = {
Expand Down
9 changes: 9 additions & 0 deletions src/resdk/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
:members:
:inherited-members:
.. autoclass:: resdk.resources.Variants
:members:
:inherited-members:
.. autoclass:: resdk.resources.User
:members:
:inherited-members:
Expand Down Expand Up @@ -102,6 +106,7 @@
from .relation import Relation
from .sample import Sample
from .user import Group, User
from .variants import Variant, VariantAnnotation, VariantCall, VariantExperiment

__all__ = (
"AnnotationField",
Expand All @@ -117,4 +122,8 @@
"Process",
"Relation",
"User",
"Variant",
"VariantAnnotation",
"VariantCall",
"VariantExperiment",
)
94 changes: 61 additions & 33 deletions src/resdk/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class BaseResource:
full_search_paramater = None
delete_warning_single = "Do you really want to delete {}?[yN]"
delete_warning_bulk = "Do you really want to delete {} objects?[yN]"
nested_serialize = False

READ_ONLY_FIELDS = ("id",)
UPDATE_PROTECTED_FIELDS = ()
Expand Down Expand Up @@ -77,44 +78,66 @@ def update(self):
response = self.api(self.id).get()
self._update_fields(response)

def __hash__(self):
"""Return hash of the object."""
return hash(self.id)

def _serialize(self):
"""Serialize the object.
By default, return the dictionary only with id or slug. The slug is used if id
does not exist yet.
"""
if self.nested_serialize:
return self._nested_serialize()
else:
return {"id": self.id} if self.id else {"slug": self.slug}

def _nested_serialize(self):
"""Nested serialize the object."""
return {"id": self.id} | {
field_name: self._dehydrate_resources(getattr(self, field_name))
for field_name in self.WRITABLE_FIELDS
if self._field_changed(field_name)
}

def _dehydrate_resources(self, obj):
"""Iterate through object and replace all objects with their ids."""
# Prevent circular imports:
from .descriptor import DescriptorSchema
from .process import Process
"""Return the serialized obj.
if isinstance(obj, DescriptorSchema) or isinstance(obj, Process):
# Slug can only be given at create requests (id not present yet)
if not self.id:
return {"slug": obj.slug}
Special attention is given to the following cases:
- obj is a BaseResource: return the serialized object.
- obj is a list: return a list of serialized objects.
- obj is a dict: replace values with serialized objects.
return {"id": obj.id}
Otherwise, return the object as is.
"""
if isinstance(obj, BaseResource):
return {"id": obj.id}
return obj._serialize()
if isinstance(obj, list):
return [self._dehydrate_resources(element) for element in obj]
return [element._dehydrate_resources(element) for element in obj]
if isinstance(obj, dict):
return {key: self._dehydrate_resources(value) for key, value in obj.items()}

return obj

def save(self):
"""Save resource to the server."""
def _field_changed(self, field_name):
"""Check if local field value is different from the server."""
current_value = getattr(self, field_name, None)
original_value = self._original_values.get(field_name, None)

def field_changed(field_name):
"""Check if local field value is different from the server."""
current_value = getattr(self, field_name, None)
original_value = self._original_values.get(field_name, None)
# The default implementation only checks for equality, since we do not support
# nested updates in most cases.
if isinstance(current_value, BaseResource) and original_value:
# TODO: Check that current and original are instances of the same resource class
return current_value.id != original_value.get("id", None)
else:
return current_value != original_value

if isinstance(current_value, BaseResource) and original_value:
# TODO: Check that current and original are instances of the same resource class
return current_value.id != original_value.get("id", None)
else:
return current_value != original_value
def save(self):
"""Save resource to the server."""

def assert_fields_unchanged(field_names):
"""Assert that fields in ``field_names`` were not changed."""
changed_fields = [name for name in field_names if field_changed(name)]
changed_fields = [name for name in field_names if self._field_changed(name)]

if changed_fields:
msg = "Not allowed to change read only fields {}".format(
Expand All @@ -129,7 +152,7 @@ def assert_fields_unchanged(field_names):

payload = {}
for field_name in self.WRITABLE_FIELDS:
if field_changed(field_name):
if self._field_changed(field_name):
payload[field_name] = self._dehydrate_resources(
getattr(self, field_name)
)
Expand Down Expand Up @@ -214,18 +237,23 @@ def __eq__(self, obj):
else:
return False

def _resource_setter(self, payload, resource, field):
"""Set ``resource`` with ``payload`` on ``field``."""
def _get_resourse(self, payload, resource):
"""Get ``resource`` from ``payload``."""
if isinstance(payload, resource):
setattr(self, field, payload)
return payload
elif isinstance(payload, dict):
setattr(self, field, resource(resolwe=self.resolwe, **payload))
return resource(resolwe=self.resolwe, **payload)
elif isinstance(payload, int):
setattr(self, field, resource.fetch_object(self.resolwe, id=payload))
return resource.fetch_object(self.resolwe, id=payload)
elif isinstance(payload, str):
setattr(self, field, resource.fetch_object(self.resolwe, slug=payload))
else:
setattr(self, field, payload)
return resource.fetch_object(self.resolwe, slug=payload)
elif isinstance(payload, list):
return [self._get_resourse(item, resource) for item in payload]
return payload

def _resource_setter(self, payload, resource, field):
"""Set ``resource`` with ``payload`` on ``field``."""
setattr(self, field, self._get_resourse(payload, resource))


class BaseResolweResource(BaseResource):
Expand Down
3 changes: 3 additions & 0 deletions src/resdk/resources/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,19 @@ def descriptor_schema(self, payload):
def sample(self):
"""Get sample."""
if self._sample is None and self._original_values.get("entity", None):
print("Sample getter not set")
# The collection data is only serialized on the top level. Replace the
# data inside 'entity' with the actual collection data.
entity_values = self._original_values["entity"].copy()
entity_values["collection"] = self._original_values.get("collection", None)
self._sample = Sample(resolwe=self.resolwe, **entity_values)
print("Sample getter", self._sample)
return self._sample

@sample.setter
def sample(self, payload):
"""Set sample."""
print("Sample setter", payload)
self._resource_setter(payload, Sample, "_sample")

@property
Expand Down
36 changes: 35 additions & 1 deletion src/resdk/resources/sample.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Sample resource."""

import logging
from typing import TYPE_CHECKING, Any, Dict, Optional
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from resdk.exceptions import ResolweServerError
from resdk.shortcuts.sample import SampleUtilsMixin

from ..utils.decorators import assert_object_exists
from .background_task import BackgroundTask
from .collection import BaseCollection, Collection
from .variants import Variant

if TYPE_CHECKING:
from .annotations import AnnotationValue
Expand Down Expand Up @@ -39,6 +40,10 @@ def __init__(self, resolwe, **model_data):
self._background = None
#: is this sample background to any other sample?
self._is_background = None
#: list of ``Variant`` objects attached to the sample
self._variants = None
#: list of ``VariantExperiment`` objects attached to the sample
self._experiments = None

super().__init__(resolwe, **model_data)

Expand All @@ -48,6 +53,8 @@ def update(self):
self._relations = None
self._background = None
self._is_background = None
self._variants = None
self._experiments = None

super().update()

Expand All @@ -60,6 +67,33 @@ def data(self):

return self._data

@property
def experiments(self):
"""Get experiments."""
if self._experiments is None:
self._experiments = self.resolwe.variant_experiment.filter(
variant_calls__sample=self.id
)
return self._experiments

@property
def latest_experiment(self):
"""Get latest experiment."""
return self.experiments.filter(ordering="-timestamp", limit=1)[0]

@property
def variants(self):
"""Get variants."""
if self._variants is None:
self._variants = self.resolwe.variant.filter(variant_calls__sample=self.id)
return self._variants

def variants_by_experiment(self, experiment):
"""Get variants for sample detected by the given experiment."""
return self.resolwe.variant.filter(
variant_calls__sample=self.id, variant_calls__experiment=experiment.id
)

@property
def collection(self):
"""Get collection."""
Expand Down
Loading

0 comments on commit 7c23807

Please sign in to comment.