forked from enviPath/enviPy
[Feature] Package Pathway Export to CSV (#439)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#439
This commit is contained in:
@ -46,6 +46,10 @@ from .models import (
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
|
||||
def get_package_for_read(user, package_uuid):
|
||||
return PackageManager.get_package_by_id(user, package_uuid)
|
||||
|
||||
|
||||
def get_package_for_write(user, package_uuid):
|
||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||
if not PackageManager.writable(user, p):
|
||||
@ -2291,3 +2295,38 @@ def predict(request, np: Form[NonPersistent]):
|
||||
return 403, {
|
||||
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
|
||||
##########
|
||||
# Export #
|
||||
##########
|
||||
class PackageExportInSchema(Schema):
|
||||
package_uuid: str
|
||||
additional_information_types: List[str] | None = None
|
||||
|
||||
|
||||
@router.get("/export", response={200: Any, 403: Error})
|
||||
def export(request, q: Query[PackageExportInSchema]):
|
||||
try:
|
||||
p = get_package_for_read(request.user, q.package_uuid)
|
||||
|
||||
from envipy_additional_information import registry
|
||||
from utilities.misc import PathwayExporter
|
||||
|
||||
ai_types = []
|
||||
if q.additional_information_types is not None:
|
||||
for ai_type in q.additional_information_types:
|
||||
if registry.get_model(ai_type) is None:
|
||||
return 400, {
|
||||
"message": f"Exporting Package with id {q.package_uuid} failed as {ai_type} is not a valid additional information type!"
|
||||
}
|
||||
ai_types.append(ai_type)
|
||||
|
||||
exporter = PathwayExporter(p, add_infs_to_export=ai_types)
|
||||
res = exporter.do_export()
|
||||
|
||||
return res
|
||||
except ValueError:
|
||||
return 403, {
|
||||
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ half_lifes.0.model }}</td>
|
||||
<td>{{ half_lifes.0.model.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -4,11 +4,13 @@ import hmac
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from django.conf import settings as s
|
||||
from envipy_additional_information import EnviPyModel, UIConfig
|
||||
from ninja import Schema
|
||||
from pydantic import HttpUrl, ValidationError
|
||||
|
||||
@ -277,15 +279,20 @@ class PackageExportSchema(Schema):
|
||||
return AdditionalInformation.objects.filter(package=obj)
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
class Exporter(ABC):
|
||||
def __init__(self, package: Package):
|
||||
self._raw_package = package
|
||||
|
||||
def do_export(self):
|
||||
return PackageExporter._export_package_as_json(self._raw_package)
|
||||
return self._export()
|
||||
|
||||
@staticmethod
|
||||
def _export_package_as_json(package: Package) -> Dict[str, Any]:
|
||||
@abstractmethod
|
||||
def _export(self):
|
||||
pass
|
||||
|
||||
|
||||
class PackageExporter(Exporter):
|
||||
def _export(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Dumps a Package and all its related objects as JSON.
|
||||
|
||||
@ -296,11 +303,126 @@ class PackageExporter:
|
||||
Dict containing the complete package data as JSON-serializable structure
|
||||
"""
|
||||
|
||||
data = PackageExportSchema.from_orm(package)
|
||||
data = PackageExportSchema.from_orm(self._raw_package)
|
||||
|
||||
return data.model_dump(mode="json")
|
||||
|
||||
|
||||
class PathwayExporter(Exporter):
|
||||
def __init__(self, package: Package, add_infs_to_export: List[str] = []):
|
||||
super().__init__(package)
|
||||
self._add_infs_to_export = add_infs_to_export
|
||||
|
||||
def _flatten_additional_information(self, ai: AdditionalInformation) -> dict[str, Any]:
|
||||
model_cls: Type[EnviPyModel] = type(ai.get())
|
||||
|
||||
def _flatten(d: dict, parent_key: str = "") -> dict[str, Any]:
|
||||
items: dict[str, Any] = {}
|
||||
for key, value in d.items():
|
||||
new_key = f"{parent_key}__{key.lower()}" if parent_key else key.lower()
|
||||
if isinstance(value, dict):
|
||||
items.update(_flatten(value, new_key))
|
||||
else:
|
||||
items[new_key] = value
|
||||
return items
|
||||
|
||||
flat = _flatten(ai.data, ai.type)
|
||||
|
||||
ui_class = getattr(model_cls, "UI", None)
|
||||
if ui_class is None:
|
||||
return flat
|
||||
|
||||
for f in model_cls.model_fields:
|
||||
ui_info = getattr(ui_class, f, None)
|
||||
|
||||
if not isinstance(ui_info, UIConfig) or ui_info.unit is None:
|
||||
continue
|
||||
|
||||
flat[f"{model_cls.__name__}__{f}__unit"] = ui_info.unit
|
||||
|
||||
return flat
|
||||
|
||||
def _export(self):
|
||||
from io import StringIO
|
||||
from csv import DictWriter
|
||||
|
||||
rows = []
|
||||
|
||||
for pw in self._raw_package.pathways.all():
|
||||
for n in pw.nodes:
|
||||
for scen in pw.scenarios.all():
|
||||
row = {
|
||||
"pathway_name": pw.name,
|
||||
"pathway_id": str(pw.url),
|
||||
"node_depth": n.depth,
|
||||
"compound_id": str(n.default_node_label.compound.url),
|
||||
"pubchem_ID": n.default_node_label.pubchem_compound_id,
|
||||
"compound_name": n.default_node_label.compound.name,
|
||||
"compound_smiles": n.default_node_label.smiles,
|
||||
"scenario_id": str(scen.url),
|
||||
"scenario_name": scen.name,
|
||||
"scenario_type": scen.scenario_type,
|
||||
"scenario_description": scen.description,
|
||||
}
|
||||
|
||||
if self._add_infs_to_export:
|
||||
ai_qs = AdditionalInformation.objects.filter(
|
||||
scenario=scen, type__in=self._add_infs_to_export
|
||||
)
|
||||
else:
|
||||
ai_qs = AdditionalInformation.objects.filter(scenario=scen)
|
||||
|
||||
for ai in ai_qs:
|
||||
if ai.type == "ProposedIntermediate" and ai.content_object == n:
|
||||
row.update({"proposed_intermediate": True})
|
||||
elif ai.type == "SpikeCompound":
|
||||
spike = {"SpikeCompound__url": ai.get().url}
|
||||
|
||||
try:
|
||||
struc = CompoundStructure.objects.get(
|
||||
compound__package=self._raw_package, url=ai.get().url
|
||||
)
|
||||
spike["SpikeCompound__smiles"] = struc.smiles
|
||||
except Exception:
|
||||
spike["SpikeCompound__smiles"] = None
|
||||
|
||||
row.update(**spike)
|
||||
else:
|
||||
row.update(self._flatten_additional_information(ai))
|
||||
rows.append(row)
|
||||
|
||||
# Get all header fields
|
||||
all_header_fields = set()
|
||||
for row in rows:
|
||||
all_header_fields.update(row.keys())
|
||||
|
||||
# Per request the CSV should start with these fields
|
||||
header = [
|
||||
"pathway_name",
|
||||
"pathway_id",
|
||||
"node_depth",
|
||||
"compound_id",
|
||||
"pubchem_ID",
|
||||
"compound_name",
|
||||
"compound_smiles",
|
||||
"scenario_id",
|
||||
"scenario_name",
|
||||
"scenario_type",
|
||||
"scenario_description",
|
||||
]
|
||||
|
||||
# User remaining fields and place them after the predefined values in a sorted manner
|
||||
remainder = sorted(list(all_header_fields.difference(set(header))))
|
||||
header.extend(remainder)
|
||||
|
||||
buffer = StringIO()
|
||||
writer = DictWriter(buffer, fieldnames=header, delimiter="\t")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
buffer.seek(0)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class PackageImporter:
|
||||
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
||||
self.preserve_uuids = preserve_uuids
|
||||
|
||||
Reference in New Issue
Block a user