Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
Expand Down
56 changes: 56 additions & 0 deletions weblab/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ def add_version(entity,
populate_entity_cache(entity)
return commit

@staticmethod
def cached_version(entity, **kwargs):
"""Add a single commit/version to an entity and return the relevant repocache entry"""
assert kwargs.get('cache', True), "Cache must be true for cached version"
version = Helpers.add_version(entity, **kwargs)
return entity.repocache.get_version(version.sha)

@staticmethod
def add_fake_version(entity, visibility='private', date=None, message='cache-only commit'):
"""Add a new commit/version only in the cache, not in git."""
Expand Down Expand Up @@ -140,6 +147,13 @@ def protocol_with_version():
return protocol


@pytest.fixture
def fittingspec_with_version():
fittingspec = recipes.fittingspec.make()
Helpers.add_version(fittingspec, visibility='private')
return fittingspec


@pytest.fixture
def public_model(helpers):
model = recipes.model.make()
Expand All @@ -154,6 +168,19 @@ def public_protocol(helpers):
return protocol


@pytest.fixture
def public_fittingspec(helpers):
fittingspec = recipes.fittingspec.make()
helpers.add_version(fittingspec, visibility='public')
return fittingspec


@pytest.fixture
def public_dataset(helpers):
dataset = recipes.dataset.make(visibility='public')
return dataset


@pytest.fixture
def moderated_model(helpers):
model = recipes.model.make()
Expand Down Expand Up @@ -331,3 +358,32 @@ def my_dataset_with_file(logged_in_user, helpers, public_protocol, client):
)
yield dataset
dataset.delete()


@pytest.fixture
def fittingresult_version(public_model, public_protocol, public_fittingspec, public_dataset):
return recipes.fittingresult_version.make(
status='SUCCESS',
fittingresult__model=public_model,
fittingresult__model_version=public_model.repocache.latest_version,
fittingresult__protocol=public_protocol,
fittingresult__protocol_version=public_protocol.repocache.latest_version,
fittingresult__fittingspec=public_fittingspec,
fittingresult__fittingspec_version=public_fittingspec.repocache.latest_version,
fittingresult__dataset=public_dataset,
)


@pytest.fixture
def fittingresult_with_result(model_with_version, protocol_with_version):
version = recipes.fittingresult_version.make(
status='SUCCESS',
fittingresult__model=model_with_version,
fittingresult__model_version=model_with_version.repocache.latest_version,
fittingresult__protocol=protocol_with_version,
fittingresult__protocol_version=protocol_with_version.repocache.latest_version,
)
version.mkdir()
with (version.abs_path / 'result.txt').open('w') as f:
f.write('fitting results')
return version
17 changes: 17 additions & 0 deletions weblab/core/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
cached_protocol_version = Recipe('CachedProtocolVersion')
cached_protocol_tag = Recipe('CachedProtocolTag')

cached_fittingspec = Recipe('CachedFittingSpec')
cached_fittingspec_version = Recipe('CachedFittingSpecVersion')
cached_fittingspec_tag = Recipe('CachedFittingSpecTag')

experiment = Recipe(
'Experiment',
model=foreign_key(model),
Expand All @@ -51,3 +55,16 @@

dataset_file = Recipe('DatasetFile',
dataset=foreign_key(dataset))

fittingresult = Recipe(
'FittingResult',
model=foreign_key(model),
model_version=foreign_key(cached_model_version),
protocol=foreign_key(protocol),
protocol_version=foreign_key(cached_protocol_version),
fittingspec=foreign_key(fittingspec),
fittingspec_version=foreign_key(cached_fittingspec_version),
dataset=foreign_key(dataset),
)

fittingresult_version = Recipe('FittingResultVersion', fittingresult=foreign_key(fittingresult))
19 changes: 12 additions & 7 deletions weblab/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,33 +140,38 @@ class Runnable(UserCreatedModelMixin, FileCollectionMixin, models.Model):
)
return_text = models.TextField(blank=True)

def __str__(self):
return '%s at %s: (%s)' % (self.experiment, self.created_at, self.status)

class Meta:
indexes = [
models.Index(fields=['created_at'])
]

def __str__(self):
return '%s at %s: (%s)' % (self.parent, self.created_at, self.status)

@property
def parent(self):
"""E.g. the Experiment this is a version of. Must be defined by subclasses."""
raise NotImplementedError

@property
def name(self):
return '{:%Y-%m-%d %H:%M:%S}'.format(self.created_at)

@property
def run_number(self):
return self.experiment.versions.filter(created_at__lte=self.created_at).count()
return self.parent.versions.filter(created_at__lte=self.created_at).count()

@property
def is_latest(self):
return not self.experiment.versions.filter(created_at__gt=self.created_at).exists()
return not self.parent.versions.filter(created_at__gt=self.created_at).exists()

@property
def visibility(self):
return self.experiment.visibility
return self.parent.visibility

@property
def viewers(self):
return self.experiment.viewers
return self.parent.viewers

@property
def abs_path(self):
Expand Down
101 changes: 53 additions & 48 deletions weblab/experiments/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,58 @@ class ProcessingException(Exception):
pass


def submit_runnable(runnable, body, user):
run = RunningExperiment.objects.create(runnable=runnable)
signature = runnable.signature

body.update({
'signature': runnable.signature,
'callBack': urljoin(settings.CALLBACK_BASE_URL, reverse('experiments:callback')),
'user': user.full_name,
'password': settings.CHASTE_PASSWORD,
'isAdmin': user.is_staff,
})

try:
response = requests.post(settings.CHASTE_URL, body)
except requests.exceptions.ConnectionError:
run.delete()
runnable.status = Runnable.STATUS_FAILED
runnable.return_text = 'Unable to connect to experiment runner service'
runnable.save()
logger.exception(runnable.return_text)
return runnable, True

res = response.content.decode().strip()
logger.debug('Response from chaste backend: %s' % res)

if not res.startswith(signature):
run.delete()
runnable.status = Runnable.STATUS_FAILED
runnable.return_text = 'Chaste backend answered with something unexpected: %s' % res
runnable.save()
logger.error(runnable.return_text)
raise ProcessingException(res)

status = res[len(signature):].strip()

if status.startswith('succ'):
run.task_id = status[4:].strip()
run.save()
elif status == 'inapplicable':
run.delete()
runnable.status = Runnable.STATUS_INAPPLICABLE
else:
run.delete()
logger.error('Chaste backend answered with error: %s' % status)
runnable.status = Runnable.STATUS_FAILED
runnable.return_text = status

runnable.save()

return runnable, True


def submit_experiment(model, model_version, protocol, protocol_version, user, rerun_ok):
"""Submit a Celery task to run an experiment.

Expand Down Expand Up @@ -80,9 +132,6 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re
author=user,
)

run = RunningExperiment.objects.create(runnable=version)
signature = version.signature

model_url = reverse(
'entities:entity_archive',
args=['model', model.pk, model_version]
Expand All @@ -94,53 +143,9 @@ def submit_experiment(model, model_version, protocol, protocol_version, user, re
body = {
'model': urljoin(settings.CALLBACK_BASE_URL, model_url),
'protocol': urljoin(settings.CALLBACK_BASE_URL, protocol_url),
'signature': signature,
'callBack': urljoin(settings.CALLBACK_BASE_URL, reverse('experiments:callback')),
'user': user.full_name,
'password': settings.CHASTE_PASSWORD,
'isAdmin': user.is_staff,
}
if protocol.is_fitting_spec:
body['dataset'] = body['fittingSpec'] = body['protocol']

try:
response = requests.post(settings.CHASTE_URL, body)
except requests.exceptions.ConnectionError:
run.delete()
version.status = Runnable.STATUS_FAILED
version.return_text = 'Unable to connect to experiment runner service'
version.save()
logger.exception(version.return_text)
return version, True

res = response.content.decode().strip()
logger.debug('Response from chaste backend: %s' % res)

if not res.startswith(signature):
run.delete()
version.status = Runnable.STATUS_FAILED
version.return_text = 'Chaste backend answered with something unexpected: %s' % res
version.save()
logger.error(version.return_text)
raise ProcessingException(res)

status = res[len(signature):].strip()

if status.startswith('succ'):
run.task_id = status[4:].strip()
run.save()
elif status == 'inapplicable':
run.delete()
version.status = Runnable.STATUS_INAPPLICABLE
else:
run.delete()
logger.error('Chaste backend answered with error: %s' % status)
version.status = Runnable.STATUS_FAILED
version.return_text = status

version.save()

return version, True
return submit_runnable(version, body, user)


def cancel_experiment(task_id):
Expand Down
55 changes: 55 additions & 0 deletions weblab/fitting/migrations/0002_auto_20200821_1339.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-08-21 13:39
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('repocache', '0021_auto_20200116_0913'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('entities', '0015_auto_20191128_1601'),
('datasets', '0005_auto_20190628_1253'),
('experiments', '0032_auto_20200317_0927'),
('fitting', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='FittingResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('dataset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fitting_results', to='datasets.Dataset')),
('fittingspec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fitting_results', to='fitting.FittingSpec')),
('fittingspec_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='fit_ver_fitres', to='repocache.CachedFittingSpecVersion')),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='model_fitting_results', to='entities.ModelEntity')),
('model_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='model_ver_fitres', to='repocache.CachedModelVersion')),
('protocol', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='protocol_fitting_results', to='entities.ProtocolEntity')),
('protocol_version', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='pro_ver_fitres', to='repocache.CachedProtocolVersion')),
],
options={
'permissions': (('run_fits', 'Can run parameter fitting experiments'),),
},
),
migrations.CreateModel(
name='FittingResultVersion',
fields=[
('runnable_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='experiments.Runnable')),
('fittingresult', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='versions', to='fitting.FittingResult')),
],
options={
'abstract': False,
},
bases=('experiments.runnable',),
),
migrations.AlterUniqueTogether(
name='fittingresult',
unique_together=set([('fittingspec', 'dataset', 'model', 'protocol', 'fittingspec_version', 'model_version', 'protocol_version')]),
),
]
Loading