diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index cf8b2f16..7c32ecf4 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -16,6 +16,7 @@ Changed Added ----- - Add ``restart`` method to the ``Data`` resource +- Add variants related models Fixed ----- diff --git a/src/resdk/query.py b/src/resdk/query.py index 3a0ceb97..ac6c80e2 100644 --- a/src/resdk/query.py +++ b/src/resdk/query.py @@ -162,24 +162,34 @@ 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.""" for key, value in filter_.items(): # 'sample' is called 'entity' in the backend. - key = key.replace("sample", "entity") + if not key.startswith("variant_calls__"): + 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 @@ -211,6 +221,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) @@ -285,6 +297,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) diff --git a/src/resdk/resolwe.py b/src/resdk/resolwe.py index 63268dc1..4a897fb3 100644 --- a/src/resdk/resolwe.py +++ b/src/resdk/resolwe.py @@ -47,6 +47,10 @@ Relation, Sample, User, + Variant, + VariantAnnotation, + VariantCall, + VariantExperiment, ) from .resources.base import BaseResource from .resources.kb import Feature, Mapping @@ -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", @@ -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 = { diff --git a/src/resdk/resources/__init__.py b/src/resdk/resources/__init__.py index 061a7964..d05d38cb 100644 --- a/src/resdk/resources/__init__.py +++ b/src/resdk/resources/__init__.py @@ -54,6 +54,10 @@ :members: :inherited-members: +.. autoclass:: resdk.resources.Variants + :members: + :inherited-members: + .. autoclass:: resdk.resources.User :members: :inherited-members: @@ -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", @@ -117,4 +122,8 @@ "Process", "Relation", "User", + "Variant", + "VariantAnnotation", + "VariantCall", + "VariantExperiment", ) diff --git a/src/resdk/resources/base.py b/src/resdk/resources/base.py index ded2ed7d..c3bfa36d 100644 --- a/src/resdk/resources/base.py +++ b/src/resdk/resources/base.py @@ -55,9 +55,12 @@ def fetch_object(cls, resolwe, id=None, slug=None): if (id is None and slug is None) or (id and slug): raise ValueError("One and only one of id or slug must be given") + print("Getting fetch object query") query = resolwe.get_query_by_resource(cls) + # print("Got query", query) if id: return query.get(id=id) + print("Getting from query with slug", slug) return query.get(slug=slug) def fields(self): @@ -77,6 +80,10 @@ 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 _dehydrate_resources(self, obj): """Iterate through object and replace all objects with their ids.""" # Prevent circular imports: @@ -130,6 +137,8 @@ def assert_fields_unchanged(field_names): payload = {} for field_name in self.WRITABLE_FIELDS: if field_changed(field_name): + print("Field changed", field_name) + print("Change", getattr(self, field_name)) payload[field_name] = self._dehydrate_resources( getattr(self, field_name) ) @@ -137,6 +146,7 @@ def assert_fields_unchanged(field_names): payload["entity"] = payload.pop("sample") if payload: + print("Sending payload 1", payload) response = self.api(self.id).patch(payload) self._update_fields(response) @@ -216,14 +226,23 @@ def __eq__(self, obj): def _resource_setter(self, payload, resource, field): """Set ``resource`` with ``payload`` on ``field``.""" + print("-" * 40) + print("Resource setter", payload, type(payload)) + print("Resource", resource) if isinstance(payload, resource): + print("resource") setattr(self, field, payload) elif isinstance(payload, dict): + print("Dict") setattr(self, field, resource(resolwe=self.resolwe, **payload)) elif isinstance(payload, int): + print("Int") setattr(self, field, resource.fetch_object(self.resolwe, id=payload)) elif isinstance(payload, str): - setattr(self, field, resource.fetch_object(self.resolwe, slug=payload)) + print("Str") + res = resource.fetch_object(self.resolwe, slug=payload) + print("Got result for Str", res) + setattr(self, field, res) else: setattr(self, field, payload) diff --git a/src/resdk/resources/data.py b/src/resdk/resources/data.py index adabde77..ac4ad34c 100644 --- a/src/resdk/resources/data.py +++ b/src/resdk/resources/data.py @@ -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 diff --git a/src/resdk/resources/sample.py b/src/resdk/resources/sample.py index 25cf7c14..8089ffaf 100644 --- a/src/resdk/resources/sample.py +++ b/src/resdk/resources/sample.py @@ -1,7 +1,7 @@ """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 @@ -9,6 +9,7 @@ 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 @@ -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) @@ -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() @@ -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.""" diff --git a/src/resdk/resources/variants.py b/src/resdk/resources/variants.py new file mode 100644 index 00000000..128f498c --- /dev/null +++ b/src/resdk/resources/variants.py @@ -0,0 +1,154 @@ +"""Variant resources.""" + +from typing import Any + +from .base import BaseResource + + +class Variant(BaseResource): + """ResolweBio Variant resource.""" + + endpoint = "variant" + + READ_ONLY_FIELDS = BaseResource.READ_ONLY_FIELDS + ( + "species", + "genome_assembly", + "chromosome", + "position", + "reference", + "alternative", + ) + + def __init__(self, resolwe, **model_data): + """Initialize object.""" + super().__init__(resolwe, **model_data) + self._annotations = None + self._samples = None + self._calls = None + + @property + def annotations(self): + """Get the annotations for this variant.""" + if self._annotations is None: + self._annotations = self.resolwe.variant_annotation.filter(variant=self.id) + return self._annotations + + @property + def samples(self): + """Get samples.""" + if self._samples is None: + self._samples = self.resolwe.sample.filter(variant_calls__variant=self.id) + return self._samples + + @property + def calls(self): + """Get variant calls associated with this variant.""" + if self._calls is None: + self._calls = self.resolwe.variant_calls.filter(variant=self.id) + return self._calls + + def __repr__(self) -> str: + """Return string representation.""" + return ( + f"Variant " + ) + + +class VariantAnnotation(BaseResource): + """VariantAnnotation resource.""" + + endpoint = "variant_annotations" + + READ_ONLY_FIELDS = BaseResource.READ_ONLY_FIELDS + ( + "variant_id", + "type", + "clinical_diagnosis", + "clinical_significance", + "dbsnp_id", + "clinvar_id", + "data", + "transcripts", + ) + + def __repr__(self) -> str: + """Return string representation.""" + return f"VariantAnnotation " + + +class VariantExperiment(BaseResource): + """Variant experiment resource.""" + + endpoint = "variant_experiment" + + READ_ONLY_FIELDS = BaseResource.READ_ONLY_FIELDS + ( + "variant_data_source", + "timestamp", + "contributor", + ) + + def __repr__(self) -> str: + """Return string representation.""" + return f"VariantExperiment " + + +class VariantCall(BaseResource): + """VariantCall resource.""" + + endpoint = "variant_calls" + + READ_ONLY_FIELDS = BaseResource.READ_ONLY_FIELDS + ( + "sample_id", + "variant_id", + "quality", + "depth_norm_quality", + "alternative_allele_depth", + "depth", + "genotype", + "genotype_quality", + "filter", + "data_id", + "experiment_id", + ) + + def __init__(self, resolwe, **model_data: Any): + """Initialize object.""" + super().__init__(resolwe, **model_data) + self._data = None + self._sample = None + self._experiment = None + self._variant = None + + @property + def data(self): + """Get the data object for this variant call.""" + if self._data is None: + self._data = self.resolwe.data.get(self.data_id) + return self._data + + @property + def sample(self): + """Get the sample object for this variant call.""" + if self._sample is None: + self._sample = self.resolwe.sample.get(self.sample_id) + return self._sample + + @property + def experiment(self): + """Get the experiment object for this variant call.""" + if self._experiment is None: + self._experiment = self.resolwe.variant_experiment.get( + id=self.experiment_id + ) + return self._experiment + + @property + def variant(self): + """Get the variant object for this variant call.""" + if self._variant is None: + self._variant = self.resolwe.variant.get(id=self.variant_id) + return self._variant + + def __repr__(self) -> str: + """Return string representation.""" + return f"VariantCall "