Skip to content

Commit 43f8e53

Browse files
committed
WIP #86
1 parent a3de734 commit 43f8e53

File tree

6 files changed

+147
-112
lines changed

6 files changed

+147
-112
lines changed

server/src/scimodom/api/management.py

Lines changed: 31 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
from scimodom.services.data import (
1010
DataService,
1111
InstantiationError,
12-
DatasetError,
12+
SelectionExistsError,
13+
DatasetExistsError,
1314
DatasetHeaderError,
1415
)
16+
from scimodom.services.importer.header import SpecsError
1517
from scimodom.services.project import ProjectService
1618
from scimodom.services.mail import get_mail_service
1719
import scimodom.utils.utils as utils
@@ -67,20 +69,18 @@ def create_project_request():
6769
@cross_origin(supports_credentials=True)
6870
@jwt_required()
6971
def add_dataset():
70-
"""Add a new dataset to a project. Parameter
71-
values are validated by DataService. Project and
72-
assembly must exist.
72+
"""Add a new dataset to a project and import data.
73+
Parameter values are validated by DataService.
7374
7475
NOTE: Permissions are not handled here. The
7576
SMID in the dataset form is coming from
7677
one of the "allowed" projects for this user, i.e.
7778
the user is only able to select from his own projects.
7879
"""
7980
dataset_form = request.json
80-
session = get_session()
8181
try:
8282
data_service = DataService.from_new(
83-
session,
83+
get_session(),
8484
dataset_form["smid"],
8585
dataset_form["title"],
8686
dataset_form["path"],
@@ -89,53 +89,36 @@ def add_dataset():
8989
technology_id=dataset_form["technology_id"],
9090
organism_id=dataset_form["organism_id"],
9191
)
92+
except SelectionExistsError:
93+
return {
94+
"message": "Invalid combination of RNA type, modification, organism, and/or technology. Modify the form and try again."
95+
}, 422
9296
except InstantiationError as exc:
93-
# no need to log these errors, users should normally handle them
94-
# ValueError during instantiation should not happen as we are using pre-defined values (Dropdown, MultiSelect, CascadeSelect)
95-
# unless database corruption...
96-
return (
97-
jsonify(
98-
{
99-
"message": f'Failed to upload dataset. Verify the input value for SMID or select a project using the button. The selected combination of modification, organism, and technology may be invalid for this project. Modify the form and try again. The message received from the server was: "{exc}"'
100-
}
101-
),
102-
500,
103-
)
97+
logger.error(f"{exc}. The request was: {dataset_form}.")
98+
return {
99+
"message": "Invalid selection. Try again or contact the administrator."
100+
}, 422
104101
except Exception as exc:
105-
# all others
106-
logger.error(
107-
f"Failed to instantiate dataservice: {exc}. The form received was: {dataset_form}."
108-
)
109-
return (
110-
jsonify(
111-
{"message": "Failed to upload dataset. Contact the administrator."}
112-
),
113-
500,
114-
)
102+
logger.error(f"{exc}. The request was: {dataset_form}.")
103+
return {"message": "Failed to create dataset. Contact the administrator."}, 500
115104

116105
try:
117106
data_service.create_dataset()
118-
# TODO: feedback to user e.g. liftover, etc. and finally successful upload (return EUFID?)
119-
except DatasetError as exc:
120-
# no need to log these errors, users should normally handle them
121-
return (
122-
jsonify(
123-
{
124-
"message": f'Failed to upload dataset. The message received from the server was: "{exc}". If you are unsure about what happened, click "Cancel" and contact the administrator.'
125-
}
126-
),
127-
500,
128-
)
129-
except DatasetHeaderError as exc:
130-
# no need to log these errors, users should normally handle them
131-
return (
132-
jsonify(
133-
{
134-
"message": f'Failed to upload dataset. Your bedRMod file header does not match the values you entered. Modify the form or the file header and try again. The message received from the server was: "{exc}". If you are unsure about what happened, click "Cancel" and contact the administrator.'
135-
}
136-
),
137-
500,
138-
)
107+
except DatasetHeaderError:
108+
return {
109+
"message": 'File upload failed. The file header does not match the value you entered for organism and/or assembly. Click "Cancel". Modify the form or the file header and start again.'
110+
}, 422
111+
except DatasetExistsError as exc:
112+
return {
113+
"message": f"File upload failed. {str(exc).replace('Aborting transaction!', '')} If you are unsure about what happened, click \"Cancel\" and contact the administrator."
114+
}, 422
115+
except EOFError as exc:
116+
return {"message": f"File upload failed. File {str(exc)} is empty!"}, 500
117+
except SpecsError as exc:
118+
return {
119+
"message": f"File upload failed. File is not conform to bedRMod specifications: {str(exc)}"
120+
}, 500
121+
139122
except Exception as exc:
140123
# TODO ...
141124
logger.error(f"Failed to create dataset: {exc}")

server/src/scimodom/services/data.py

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from pathlib import Path
33
from typing import ClassVar, Any
44

5-
from sqlalchemy import select, func
5+
from sqlalchemy import select, exists, func
66
from sqlalchemy.orm import Session
7+
from sqlalchemy.exc import NoResultFound
78

89
import scimodom.database.queries as queries
910
import scimodom.utils.specifications as specs
@@ -33,7 +34,15 @@ class InstantiationError(Exception):
3334
pass
3435

3536

36-
class DatasetError(Exception):
37+
class SelectionExistsError(Exception):
38+
"""Exception handling for Dataset instantiation
39+
(from new) with a choice of modification, organism,
40+
and technology that does not exists."""
41+
42+
pass
43+
44+
45+
class DatasetExistsError(Exception):
3746
"""Exception for handling Dataset instantiation,
3847
e.g. suspected duplicate entries."""
3948

@@ -102,38 +111,39 @@ def __init__(
102111

103112
selection_id = kwargs.get("selection_id", None)
104113
if selection_id is not None:
105-
query = select(Selection.id)
106-
ids = self._session.execute(query).scalars().all()
107114
for sid in selection_id:
108-
if sid not in ids:
115+
is_found = self._session.query(
116+
exists().where(Selection.id == sid)
117+
).scalar()
118+
if not is_found:
109119
msg = f"Selection ID = {sid} not found! Aborting transaction!"
110-
raise ValueError(msg)
120+
raise InstantiationError(msg)
111121
self._selection_id = selection_id
112122
self._set_ids()
113123
else:
114124
modification_id = kwargs.get("modification_id", None)
115-
query = select(Modification.id)
116-
ids = self._session.execute(query).scalars().all()
117125
for mid in modification_id:
118-
if mid not in ids:
126+
try:
127+
_ = self._modification_id_to_name(mid)
128+
except NoResultFound:
119129
msg = f"Modification ID = {mid} not found! Aborting transaction!"
120-
raise ValueError(msg)
130+
raise InstantiationError(msg)
121131
self._modification_id = modification_id
122132
technology_id = kwargs.get("technology_id", None)
123-
query = select(DetectionTechnology.id)
124-
ids = self._session.execute(query).scalars().all()
125-
if technology_id not in ids:
133+
try:
134+
_ = self._technology_id_to_tech(technology_id)
135+
except NoResultFound:
126136
msg = (
127137
f"Technology ID = {technology_id} not found! Aborting transaction!"
128138
)
129-
raise ValueError(msg)
139+
raise InstantiationError(msg)
130140
self._technology_id = technology_id
131141
organism_id = kwargs.get("organism_id", None)
132-
query = select(Organism.id)
133-
ids = self._session.execute(query).scalars().all()
134-
if organism_id not in ids:
142+
try:
143+
_ = self._organism_id_to_organism(organism_id)
144+
except NoResultFound:
135145
msg = f"Organism ID = {organism_id} not found! Aborting transaction!"
136-
raise ValueError(msg)
146+
raise InstantiationError(msg)
137147
self._organism_id = organism_id
138148
self._set_selection()
139149

@@ -381,7 +391,11 @@ def get_eufid(self) -> str:
381391

382392
def _set_ids(self) -> None:
383393
"""Set modification_id, technology_id,
384-
and organism_id from selection_id."""
394+
and organism_id from selection_id.
395+
396+
A dataset can be associated with more
397+
than one selection_id, but organism_id
398+
and technology_id must be identical."""
385399
modification_id: list = []
386400
technology_id: int
387401
organism_id: int
@@ -414,9 +428,11 @@ def _set_ids(self) -> None:
414428
except:
415429
technology_id = selection[2]
416430
if technology_id != selection[2]:
431+
tech1 = self._technology_id_to_tech(technology_id)
432+
tech2 = self._technology_id_to_tech(selection[2])
417433
msg = (
418-
f"Two different technology IDs {technology_id} and "
419-
f"{selection[2]} are associated with this dataset. "
434+
f"Different technologies {tech1} and {tech2} "
435+
"cannot be associated with the same dataset. "
420436
"Aborting transaction!"
421437
)
422438
raise InstantiationError(msg)
@@ -425,26 +441,34 @@ def _set_ids(self) -> None:
425441
except:
426442
organism_id = selection[3]
427443
if organism_id != selection[3]:
444+
cto1, org_name1 = self._organism_id_to_organism(organism_id)
445+
cto2, org_name2 = self._organism_id_to_organism(selection[3])
428446
msg = (
429-
f"Two different organism IDs {organism_id} and "
430-
f"{selection[3]} are associated with this dataset. "
431-
"Aborting transaction!"
447+
f"Different organisms {org_name1} ({cto1}) and "
448+
f"{org_name2} ({cto2}) cannot be associated with the "
449+
"same dataset. Aborting transaction!"
432450
)
433451
raise InstantiationError(msg)
434452
self._association[selection[1]] = selection_id
435453
# this cannot actually happen...
436454
if len(set(modification_id)) != len(modification_id):
455+
m_names = [self._modification_id_to_name(mid) for mid in modification_id]
437456
msg = (
438-
f"Repeated modification IDs {','.join([i for i in modification_id])} are "
439-
"associated with this dataset. Aborting transaction!"
457+
f"Repeated modifications {','.join([m for m in m_names])} "
458+
"cannot be associated with the same dataset. Aborting transaction!"
440459
)
441460
raise InstantiationError(msg)
442461
self._modification_id = modification_id
443462
self._technology_id = technology_id
444463
self._organism_id = organism_id
445464

446465
def _set_selection(self) -> None:
447-
"""Set selection IDs associated with dataset."""
466+
"""Set selection IDs associated with a
467+
dataset. Depending on the choice of
468+
modification_id(s), organism_id, and
469+
technology_id, a selection_id may or may
470+
not exists. If it does not exists, a
471+
SelectionExistsError is raised."""
448472
selection_id = []
449473
for modification_id in self._modification_id:
450474
query = queries.query_column_where(
@@ -458,12 +482,16 @@ def _set_selection(self) -> None:
458482
)
459483
try:
460484
selection_id.append(self._session.execute(query).scalar_one())
461-
except Exception as exc:
485+
except NoResultFound as exc:
486+
m_name = self._modification_id_to_name(modification_id)
487+
tech = self._technology_id_to_tech(self._technology_id)
488+
cto, org_name = self._organism_id_to_organism(self._organism_id)
462489
msg = (
463-
f"Selection (mod={modification_id}, tech={self._technology_id}, "
464-
f"organism={self._organism_id}) does not exists. Aborting transaction!"
490+
f"Selection (mod={m_name}, tech={tech}, "
491+
f"organism=({org_name}, {cto})) does not exists. "
492+
"Aborting transaction!"
465493
)
466-
raise InstantiationError(msg) from exc
494+
raise SelectionExistsError(msg) from exc
467495
query = (
468496
select(
469497
Modomics.short_name,
@@ -498,11 +526,11 @@ def _validate_entry(self) -> None:
498526
eufid = self._session.execute(query).scalar_one_or_none()
499527
if eufid:
500528
msg = (
501-
f"A similar record with EUFID = {eufid} already exists for project {self._smid} "
502-
f"with title = {self._title}, and the following selection ID {selection_id}. "
529+
f"Suspected duplicate record with EUFID = {eufid} (SMID = {self._smid}), "
530+
f'title = "{self._title}", and selection ID = {selection_id}. '
503531
f"Aborting transaction!"
504532
)
505-
raise DatasetError(msg)
533+
raise DatasetExistsError(msg)
506534

507535
def _add_association(self) -> None:
508536
"""Create new association entry for dataset."""
@@ -518,3 +546,38 @@ def _create_eufid(self) -> None:
518546
query = select(Dataset.id)
519547
eufids = self._session.execute(query).scalars().all()
520548
self._eufid = utils.gen_short_uuid(self.EUFID_LENGTH, eufids)
549+
550+
def _modification_id_to_name(self, idx: int) -> str:
551+
"""Retrieve modification name for id.
552+
553+
:param idx: id (PK)
554+
:type idx: int
555+
"""
556+
query = (
557+
select(Modomics.short_name)
558+
.join(Modification, Modomics.modifications)
559+
.where(Modification.id == idx)
560+
)
561+
return self._session.execute(query).scalar_one()
562+
563+
def _organism_id_to_organism(self, idx: int) -> str:
564+
"""Retrieve cto and organism name for id.
565+
566+
:param idx: id (PK)
567+
:type idx: int
568+
"""
569+
query = (
570+
select(Organism.cto, Taxa.short_name)
571+
.join(Taxa, Organism.inst_taxa)
572+
.where(Organism.id == idx)
573+
)
574+
return self._session.execute(query).one()
575+
576+
def _technology_id_to_tech(self, idx: int) -> str:
577+
"""Retrieve technology name for id.
578+
579+
:param idx: id (PK)
580+
:type idx: int
581+
"""
582+
query = select(DetectionTechnology.tech).where(DetectionTechnology.id == idx)
583+
return self._session.execute(query).scalar_one()

server/src/scimodom/services/importer/data.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
import scimodom.utils.utils as utils
99

1010

11-
class SpecsError(Exception):
12-
"""Exception handling for specification errors."""
13-
14-
pass
15-
16-
1711
class EUFDataImporter(BaseImporter):
1812
"""Utility class to import bedRMod (EU) formatted files.
1913
This class only handles the actual records, not the header,

server/src/scimodom/services/importer/generic.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@
88
import scimodom.utils.utils as utils
99

1010

11-
class SpecsError(Exception):
12-
"""Exception handling for specification errors."""
13-
14-
pass
15-
16-
1711
class BEDImporter(BaseImporter):
1812
"""Utility class to import BED formatted files.
1913
BED6+ files, incl. bedRMod (EUF) are cut down

0 commit comments

Comments
 (0)