Skip to content

Commit

Permalink
Merge branch 'master' into mip_family_id_compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
henrikstranneheim committed Aug 26, 2020
2 parents 30e81ba + e9405da commit 2bbd47f
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 6.5.0
current_version = 6.7.0
commit = True
tag = True
tag_name = {new_version}
Expand Down
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
* @jemten @b4ckm4n
* @jemten @b4ckm4n @henrikstranneheim
2 changes: 1 addition & 1 deletion nuxt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trailblazer",
"version": "6.5.0",
"version": "6.7.0",
"description": "Trailblazer web UI",
"author": "Robin Andeer <robin.andeer@gmail.com>",
"private": true,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def run_tests(self):
# Versions should comply with PEP440. For a discussion on
# single-sourcing the version across setup.py and the project code,
# see http://packaging.python.org/en/latest/tutorial.html#version
version='6.5.0',
version='6.7.0',

description=('Track MIP analyses.'),
long_description=__doc__,
Expand Down
94 changes: 77 additions & 17 deletions tests/store/test_store_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_track_update(store):
def test_add_user(store):
# GIVEN an empty database
assert store.User.query.first() is None
name, email = 'Paul T. Anderson', 'paul.anderson@magnolia.com'
name, email = "Paul T. Anderson", "paul.anderson@magnolia.com"
# WHEN adding a new user
new_user = store.add_user(name, email)
# THEN it should be stored in the database
Expand All @@ -33,7 +33,7 @@ def test_add_user(store):

def test_user(store):
# GIVEN a database with a user
name, email = 'Paul T. Anderson', 'paul.anderson@magnolia.com'
name, email = "Paul T. Anderson", "paul.anderson@magnolia.com"
store.add_user(name, email)
assert store.User.query.filter_by(email=email).first().email == email
# WHEN querying for a user
Expand All @@ -42,7 +42,7 @@ def test_user(store):
assert user_obj.email == email

# WHEN querying for a user that doesn't exist
user_obj = store.user('this_is_a_made_up_email@fake_example.com')
user_obj = store.user("this_is_a_made_up_email@fake_example.com")
# THEN it should return as None
assert user_obj is None

Expand All @@ -63,20 +63,80 @@ def test_analysis(sample_store):
assert analysis_obj is None


@pytest.mark.parametrize('family, expected_bool', [
('crazygoat', True), # running
('nicemouse', False), # completed
('politesnake', False), # failed
('gentlebird', True), # pending
])
def test_is_running(sample_store, family, expected_bool):
@pytest.mark.parametrize(
"family, expected_bool",
[
("crazygoat", True), # running
("nicemouse", False), # completed
("politesnake", False), # failed
("gentlebird", True), # pending
],
)
def test_is_latest_analysis_ongoing(sample_store, family, expected_bool):
# GIVEN an analysis
analysis_objs = sample_store.analyses(family=family).first()
assert analysis_objs is not None
# WHEN checking if the family has a running analysis
is_running = sample_store.is_running(family)
# WHEN checking if the family has an ongoing analysis status
is_ongoing = sample_store.is_latest_analysis_ongoing(family)
# THEN it should return the expected result
assert is_running is expected_bool
assert is_ongoing is expected_bool


@pytest.mark.parametrize(
"family, expected_bool",
[
("crazygoat", False), # running
("nicemouse", False), # completed
("politesnake", True), # failed
("gentlebird", False), # pending
],
)
def test_is_latest_analysis_failed(sample_store, family, expected_bool):
# GIVEN an analysis
analysis_objs = sample_store.analyses(family=family).first()
assert analysis_objs is not None
# WHEN checking if the family has a failed analysis status
is_failed = sample_store.is_latest_analysis_failed(family)
# THEN it should return the expected result
assert is_failed is expected_bool


@pytest.mark.parametrize(
"family, expected_bool",
[
("crazygoat", False), # running
("nicemouse", True), # completed
("politesnake", False), # failed
("gentlebird", False), # pending
],
)
def test_is_latest_analysis_completed(sample_store, family, expected_bool):
# GIVEN an analysis
analysis_objs = sample_store.analyses(family=family).first()
assert analysis_objs is not None
# WHEN checking if the family has a failed analysis status
is_failed = sample_store.is_latest_analysis_completed(family)
# THEN it should return the expected result
assert is_failed is expected_bool


@pytest.mark.parametrize(
"family, expected_status",
[
("crazygoat", "running"),
("nicemouse", "completed"),
("politesnake", "failed"),
("gentlebird", "pending"),
],
)
def test_get_latest_analysis_status(sample_store, family, expected_status):
# GIVEN an analysis
analysis_objs = sample_store.analyses(family=family).first()
assert analysis_objs is not None
# WHEN checking if the family has an analysis status
status = sample_store.get_latest_analysis_status(family)
# THEN it should return the expected result
assert status is expected_status


def test_aggregate_jobs(sample_store):
Expand All @@ -94,8 +154,8 @@ def test_aggregate_jobs(sample_store):

# ... it should exclude "cancelled" jobs
assert len(jobs_data) == 1
assert jobs_data[0]['name'] == 'samtools_mpileup'
assert jobs_data[0]['count'] == 1
assert jobs_data[0]["name"] == "samtools_mpileup"
assert jobs_data[0]["count"] == 1


def test_aggregate_jobs_since_forever_date(sample_store):
Expand All @@ -112,8 +172,8 @@ def test_aggregate_jobs_since_forever_date(sample_store):

# THEN it should return a list of dicts per job type with count
assert len(jobs_data) == 1
assert jobs_data[0]['name'] == 'samtools_mpileup'
assert jobs_data[0]['count'] == 1
assert jobs_data[0]["name"] == "samtools_mpileup"
assert jobs_data[0]["count"] == 1


def test_aggregate_jobs_since_yesterday(sample_store):
Expand Down
28 changes: 14 additions & 14 deletions trailblazer/cli/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,43 @@


@click.command()
@click.option('-f', '--force', is_flag=True, help='skip sanity checks')
@click.option('-y', '--yes', is_flag=True, help='skip manual confirmations')
@click.argument('analysis_id', type=int)
@click.option("-f", "--force", is_flag=True, help="skip sanity checks")
@click.option("-y", "--yes", is_flag=True, help="skip manual confirmations")
@click.argument("analysis_id", type=int)
@click.pass_context
def delete(context, force, yes, analysis_id):
"""Mark analysis as deleted in db and remove analysis folder on disk."""
analysis_obj = context.obj['store'].analysis(analysis_id)
analysis_obj = context.obj["store"].analysis(analysis_id)
if analysis_obj is None:
print(click.style('analysis log not found', fg='red'))
print(click.style("analysis log not found", fg="red"))
context.abort()

print(click.style(f"{analysis_obj.family}: {analysis_obj.status}"))

if analysis_obj.is_temp:
if analysis_obj.has_ongoing_status:
if yes or click.confirm(f"remove analysis log?"):
analysis_obj.delete()
context.obj['store'].commit()
print(click.style(f"analysis deleted: {analysis_obj.family}", fg='blue'))
context.obj["store"].commit()
print(click.style(f"analysis deleted: {analysis_obj.family}", fg="blue"))
else:
if analysis_obj.is_deleted:
print(click.style(f"{analysis_obj.family}: already deleted", fg='red'))
print(click.style(f"{analysis_obj.family}: already deleted", fg="red"))
context.abort()

if Path(analysis_obj.out_dir).exists() or force:
root_dir = context.obj['store'].families_dir
root_dir = context.obj["store"].families_dir
family_dir = analysis_obj.out_dir
if not force and (len(family_dir) <= len(root_dir) or root_dir not in family_dir):
print(click.style(f"unknown analysis output dir: {analysis_obj.out_dir}", fg='red'))
print(click.style(f"unknown analysis output dir: {analysis_obj.out_dir}", fg="red"))
print(click.style("use '--force' to override"))
context.abort()

if yes or click.confirm(f"remove analysis output: {analysis_obj.out_dir}?"):
shutil.rmtree(analysis_obj.out_dir, ignore_errors=True)
analysis_obj.is_deleted = True
context.obj['store'].commit()
print(click.style(f"analysis deleted: {analysis_obj.family}", fg='blue'))
context.obj["store"].commit()
print(click.style(f"analysis deleted: {analysis_obj.family}", fg="blue"))
else:
print(click.style(f"analysis output doesn't exist: {analysis_obj.out_dir}", fg='red'))
print(click.style(f"analysis output doesn't exist: {analysis_obj.out_dir}", fg="red"))
print(click.style("use '--force' to override"))
context.abort()
6 changes: 4 additions & 2 deletions trailblazer/constants.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
TEMP_STATUSES = ('pending', 'running')
DEFAULT_CAPTURE_KIT = 'agilent_sureselect_cre.v1'
DEFAULT_CAPTURE_KIT = "agilent_sureselect_cre.v1"
COMPLETED_STATUS = "completed"
FAILED_STATUS = "failed"
ONGOING_STATUSES = ("pending", "running")
33 changes: 27 additions & 6 deletions trailblazer/store/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sqlalchemy as sqa

from trailblazer.mip.config import ConfigHandler
from trailblazer.constants import TEMP_STATUSES
from trailblazer.constants import COMPLETED_STATUS, FAILED_STATUS, ONGOING_STATUSES
from . import models


Expand Down Expand Up @@ -60,7 +60,7 @@ def analyses(
if isinstance(deleted, bool):
analysis_query = analysis_query.filter_by(is_deleted=deleted)
if temp:
analysis_query = analysis_query.filter(self.Analysis.status.in_(TEMP_STATUSES))
analysis_query = analysis_query.filter(self.Analysis.status.in_(ONGOING_STATUSES))
if before:
analysis_query = analysis_query.filter(self.Analysis.started_at < before)
if is_visible is not None:
Expand All @@ -77,10 +77,31 @@ def track_update(self):
metadata.updated_at = dt.datetime.now()
self.commit()

def is_running(self, family: str) -> bool:
"""Check if an analysis is currently running/pending for a family."""
latest_analysis = self.analyses(family=family).first()
return latest_analysis and latest_analysis.status in TEMP_STATUSES
def is_latest_analysis_ongoing(self, case_id: str) -> bool:
"""Check if the latest analysis is ongoing for a case_id"""
latest_analysis = self.analyses(family=case_id).first()
if latest_analysis and latest_analysis.status in ONGOING_STATUSES:
return True
return False

def is_latest_analysis_failed(self, case_id: str) -> bool:
"""Check if the latest analysis is failed for a case_id"""
latest_analysis = self.analyses(family=case_id).first()
if latest_analysis and latest_analysis.status == FAILED_STATUS:
return True
return False

def get_latest_analysis_status(self, case_id: str) -> str:
"""Get latest analysis status for a case_id"""
latest_analysis = self.analyses(family=case_id).first()
return latest_analysis.status

def is_latest_analysis_completed(self, case_id: str) -> bool:
"""Check if the latest analysis is completed for a case_id"""
latest_analysis = self.analyses(family=case_id).first()
if latest_analysis and latest_analysis.status == COMPLETED_STATUS:
return True
return False

def info(self) -> models.Info:
"""Return metadata entry."""
Expand Down
37 changes: 19 additions & 18 deletions trailblazer/store/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from sqlalchemy import Column, ForeignKey, orm, types, UniqueConstraint

from trailblazer.mip import sacct
from trailblazer.constants import TEMP_STATUSES
from trailblazer.constants import ONGOING_STATUSES

STATUS_OPTIONS = ('pending', 'running', 'completed', 'failed', 'error', 'canceled')
STATUS_OPTIONS = ("pending", "running", "completed", "failed", "error", "canceled")
JOB_STATUS_OPTIONS = [category.lower() for category in sacct.CATEGORIES]
PRIORITY_OPTIONS = ('low', 'normal', 'high')
TYPES = ('wes', 'wgs', 'rna')
PRIORITY_OPTIONS = ("low", "normal", "high")
TYPES = ("wes", "wgs", "rna")

Model = alchy.make_declarative_base(Base=alchy.ModelBase)

Expand All @@ -19,7 +19,7 @@ class Info(Model):

"""Keep track of meta data."""

__tablename__ = 'info'
__tablename__ = "info"

id = Column(types.Integer, primary_key=True)
created_at = Column(types.DateTime, default=datetime.datetime.now)
Expand All @@ -28,7 +28,7 @@ class Info(Model):

class User(Model):

__tablename__ = 'user'
__tablename__ = "user"

id = Column(types.Integer, primary_key=True)
google_id = Column(types.String(128), unique=True)
Expand All @@ -37,21 +37,22 @@ class User(Model):
avatar = Column(types.Text)
created_at = Column(types.DateTime, default=datetime.datetime.now)

runs = orm.relationship('Analysis', backref='user')
runs = orm.relationship("Analysis", backref="user")

@property
def first_name(self) -> str:
"""First part of name."""
return self.name.split(' ')[0]
return self.name.split(" ")[0]


class Analysis(Model):

"""Analysis record."""

__tablename__ = 'analysis'
__table_args__ = (UniqueConstraint('family', 'started_at', 'status',
name='_uc_family_start_status'),)
__tablename__ = "analysis"
__table_args__ = (
UniqueConstraint("family", "started_at", "status", name="_uc_family_start_status"),
)

id = Column(types.Integer, primary_key=True)
family = Column(types.String(128), nullable=False)
Expand All @@ -69,24 +70,24 @@ class Analysis(Model):
is_visible = Column(types.Boolean, default=True)
type = Column(types.Enum(*TYPES))
user_id = Column(ForeignKey(User.id))
progress = Column(types.Float, default=0.)
progress = Column(types.Float, default=0.0)

failed_jobs = orm.relationship('Job', backref='analysis')
failed_jobs = orm.relationship("Job", backref="analysis")

@property
def is_temp(self):
"""Check if the log is for a temporary status: running/pending."""
return self.status in TEMP_STATUSES
def has_ongoing_status(self):
"""Check if the log has an ongoing status: 'running|'pending'"""
return self.status in ONGOING_STATUSES


class Job(Model):

"""Represent a step in the pipeline."""

__tablename__ = 'job'
__tablename__ = "job"

id = Column(types.Integer, primary_key=True)
analysis_id = Column(ForeignKey(Analysis.id, ondelete='CASCADE'), nullable=False)
analysis_id = Column(ForeignKey(Analysis.id, ondelete="CASCADE"), nullable=False)
slurm_id = Column(types.Integer)
name = Column(types.String(64))
context = Column(types.String(64))
Expand Down

0 comments on commit 2bbd47f

Please sign in to comment.