diff --git a/backend/composer/admin.py b/backend/composer/admin.py index 3d9ed946..017a5604 100644 --- a/backend/composer/admin.py +++ b/backend/composer/admin.py @@ -19,7 +19,8 @@ Tag, Via, FunctionalCircuitRole, - ProjectionPhenotype, Destination, Synonym, AnatomicalEntityMeta, Layer, Region, AnatomicalEntityIntersection + ProjectionPhenotype, Destination, Synonym, AnatomicalEntityMeta, Layer, Region, AnatomicalEntityIntersection, + AnatomicalEntity ) @@ -117,10 +118,9 @@ class AnatomicalEntityIntersectionAdmin(admin.ModelAdmin): raw_id_fields = ('layer', 'region',) -class AnatomicalEntityAdmin(admin.ModelAdmin): - list_display = ('name', 'ontology_uri', 'region_layer',) - search_fields = ('name',) - raw_id_fields = ('region_layer',) +class AnatomicalEntityNewAdmin(admin.ModelAdmin): + raw_id_fields = ('simple_entity', 'region_layer') + search_fields = ('simple_entity', 'region_layer') inlines = [SynonymInline] @@ -232,7 +232,7 @@ def get_form(self, request, obj=None, change=False, **kwargs): admin.site.register(Layer, LayerAdmin) admin.site.register(Region, RegionAdmin) admin.site.register(AnatomicalEntityIntersection, AnatomicalEntityIntersectionAdmin) -admin.site.register(AnatomicalEntity, AnatomicalEntityAdmin) +admin.site.register(AnatomicalEntity, AnatomicalEntityNewAdmin) admin.site.register(Phenotype) admin.site.register(Sex) admin.site.register(ConnectivityStatement, ConnectivityStatementAdmin) diff --git a/backend/composer/management/commands/ingest_statements.py b/backend/composer/management/commands/ingest_statements.py index 09ec24ea..024ffd58 100644 --- a/backend/composer/management/commands/ingest_statements.py +++ b/backend/composer/management/commands/ingest_statements.py @@ -13,13 +13,19 @@ def add_arguments(self, parser): action='store_true', help='Set this flag to update upstream statements.', ) + parser.add_argument( + '--update_anatomic_entities', + action='store_true', + help='Set this flag to try move anatomical entities to specific layer, region.', + ) def handle(self, *args, **options): update_upstream = options['update_upstream'] + update_anatomic_entities = options['update_anatomic_entities'] start_time = time.time() - ingest_statements(update_upstream) + ingest_statements(update_upstream, update_anatomic_entities) end_time = time.time() diff --git a/backend/composer/migrations/0046_anatomicalentityintersection_anatomicalentitymeta_and_more.py b/backend/composer/migrations/0046_anatomicalentityintersection_anatomicalentitymeta_and_more.py index b7daa508..45fd0a32 100644 --- a/backend/composer/migrations/0046_anatomicalentityintersection_anatomicalentitymeta_and_more.py +++ b/backend/composer/migrations/0046_anatomicalentityintersection_anatomicalentitymeta_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2024-03-19 14:14 +# Generated by Django 4.1.4 on 2024-03-20 13:39 from django.db import migrations, models import django.db.models.deletion @@ -40,14 +40,19 @@ class Migration(migrations.Migration): ("ontology_uri", models.URLField(unique=True)), ], options={ - "verbose_name_plural": "Anatomical Entities", + "verbose_name_plural": "Anatomical Entities Meta", "ordering": ["name"], }, ), migrations.AlterField( - model_name="anatomicalentity", - name="ontology_uri", - field=models.URLField(), + model_name="synonym", + name="anatomical_entity", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="synonyms", + to="composer.anatomicalentity", + ), ), migrations.CreateModel( name="Layer", @@ -66,42 +71,16 @@ class Migration(migrations.Migration): ], bases=("composer.anatomicalentitymeta",), ), - migrations.CreateModel( - name="Region", - fields=[ - ( - "anatomicalentitymeta_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="composer.anatomicalentitymeta", - ), - ), - ( - "layers", - models.ManyToManyField( - through="composer.AnatomicalEntityIntersection", - to="composer.layer", - ), - ), - ], - bases=("composer.anatomicalentitymeta",), - ), migrations.CreateModel( name="AnatomicalEntityNew", fields=[ ( - "anatomicalentitymeta_ptr", - models.OneToOneField( + "id", + models.BigAutoField( auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, primary_key=True, serialize=False, - to="composer.anatomicalentitymeta", + verbose_name="ID", ), ), ( @@ -112,22 +91,16 @@ class Migration(migrations.Migration): to="composer.anatomicalentityintersection", ), ), + ( + "simple_entity", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="composer.anatomicalentitymeta", + ), + ), ], - bases=("composer.anatomicalentitymeta",), - ), - migrations.AddField( - model_name="anatomicalentityintersection", - name="layer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="composer.layer" - ), - ), - migrations.AddField( - model_name="anatomicalentityintersection", - name="region", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="composer.region" - ), ), migrations.AddField( model_name="connectivitystatement", @@ -165,7 +138,7 @@ class Migration(migrations.Migration): name="anatomical_entities_new", field=models.ManyToManyField( blank=True, - related_name="via_connection_layers_new", + related_name="via_connection_layers", to="composer.anatomicalentitynew", ), ), @@ -174,4 +147,42 @@ class Migration(migrations.Migration): name="from_entities_new", field=models.ManyToManyField(blank=True, to="composer.anatomicalentitynew"), ), + migrations.CreateModel( + name="Region", + fields=[ + ( + "anatomicalentitymeta_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="composer.anatomicalentitymeta", + ), + ), + ( + "layers", + models.ManyToManyField( + through="composer.AnatomicalEntityIntersection", + to="composer.layer", + ), + ), + ], + bases=("composer.anatomicalentitymeta",), + ), + migrations.AddField( + model_name="anatomicalentityintersection", + name="layer", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="composer.layer" + ), + ), + migrations.AddField( + model_name="anatomicalentityintersection", + name="region", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="composer.region" + ), + ), ] diff --git a/backend/composer/migrations/0047_auto_20240319_1414.py b/backend/composer/migrations/0047_auto_20240320_1340.py similarity index 83% rename from backend/composer/migrations/0047_auto_20240319_1414.py rename to backend/composer/migrations/0047_auto_20240320_1340.py index eaabf5d6..30b78c3f 100644 --- a/backend/composer/migrations/0047_auto_20240319_1414.py +++ b/backend/composer/migrations/0047_auto_20240320_1340.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2024-03-19 14:07 +# Generated by Django 4.1.4 on 2024-03-20 13:40 from django.db import migrations @@ -6,6 +6,7 @@ def copy_anatomical_entities_to_new(apps, schema_editor): AnatomicalEntity = apps.get_model('composer', 'AnatomicalEntity') AnatomicalEntityNew = apps.get_model('composer', 'AnatomicalEntityNew') + AnatomicalEntityMeta = apps.get_model('composer', 'AnatomicalEntityMeta') Destination = apps.get_model('composer', 'Destination') Via = apps.get_model('composer', 'Via') ConnectivityStatement = apps.get_model('composer', 'ConnectivityStatement') @@ -13,11 +14,13 @@ def copy_anatomical_entities_to_new(apps, schema_editor): # Copy AnatomicalEntity instances to AnatomicalEntityNew for entity in AnatomicalEntity.objects.all(): - new_entity = AnatomicalEntityNew.objects.create( + new_entity_meta = AnatomicalEntityMeta.objects.create( name=entity.name, ontology_uri=entity.ontology_uri, ) + new_entity = AnatomicalEntityNew.objects.create(simple_entity=new_entity_meta) + # Update ManyToMany relationships for Destination for destination in Destination.objects.filter(anatomical_entities=entity): destination.anatomical_entities_new.add(new_entity) @@ -46,6 +49,4 @@ class Migration(migrations.Migration): ("composer", "0046_anatomicalentityintersection_anatomicalentitymeta_and_more"), ] - operations = [ - migrations.RunPython(copy_anatomical_entities_to_new), - ] + operations = [migrations.RunPython(copy_anatomical_entities_to_new), ] diff --git a/backend/composer/migrations/0048_remove_connectivitystatement_origins_and_more.py b/backend/composer/migrations/0048_remove_connectivitystatement_origins_and_more.py index cd183d8b..b5721be0 100644 --- a/backend/composer/migrations/0048_remove_connectivitystatement_origins_and_more.py +++ b/backend/composer/migrations/0048_remove_connectivitystatement_origins_and_more.py @@ -1,11 +1,11 @@ -# Generated by Django 4.1.4 on 2024-03-19 16:03 +# Generated by Django 4.1.4 on 2024-03-20 14:13 from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ("composer", "0047_auto_20240319_1414"), + ("composer", "0047_auto_20240320_1340"), ] operations = [ diff --git a/backend/composer/migrations/0049_rename_anatomicalentitynew_anatomicalentity_and_more.py b/backend/composer/migrations/0049_rename_anatomicalentitynew_anatomicalentity_and_more.py index b2fc93d5..3df00901 100644 --- a/backend/composer/migrations/0049_rename_anatomicalentitynew_anatomicalentity_and_more.py +++ b/backend/composer/migrations/0049_rename_anatomicalentitynew_anatomicalentity_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2024-03-19 16:08 +# Generated by Django 4.1.4 on 2024-03-20 14:14 from django.db import migrations, models import django.db.models.deletion @@ -19,16 +19,16 @@ class Migration(migrations.Migration): old_name="from_entities_new", new_name="from_entities", ), + migrations.RenameField( + model_name="via", + old_name="anatomical_entities_new", + new_name="anatomical_entities", + ), migrations.RenameField( model_name="via", old_name="from_entities_new", new_name="from_entities", ), - # Assuming the intention was to rename 'origins_new' to 'origins', - # 'anatomical_entities_new' to 'anatomical_entities', and similar for other fields - # Note: For ManyToManyFields, if Django's migration system doesn't support direct renaming, - # you may need a more complex approach to adjust the intermediary table instead of simple field renaming. - # For now, let's assume renaming is what you intended: migrations.RenameField( model_name="connectivitystatement", old_name="origins_new", @@ -39,19 +39,17 @@ class Migration(migrations.Migration): old_name="anatomical_entities_new", new_name="anatomical_entities", ), - migrations.RenameField( - model_name="via", - old_name="anatomical_entities_new", - new_name="anatomical_entities", - ), migrations.RenameField( model_name="synonym", old_name="anatomical_entity_new", new_name="anatomical_entity", ), + migrations.AlterUniqueTogether( + name="synonym", + unique_together=set(), + ), migrations.AlterUniqueTogether( name="synonym", unique_together={("anatomical_entity", "name")}, ), ] - diff --git a/backend/composer/migrations/0050_alter_anatomicalentity_options_and_more.py b/backend/composer/migrations/0050_alter_anatomicalentity_options_and_more.py deleted file mode 100644 index 4abfb389..00000000 --- a/backend/composer/migrations/0050_alter_anatomicalentity_options_and_more.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 4.1.4 on 2024-03-19 16:14 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ("composer", "0049_rename_anatomicalentitynew_anatomicalentity_and_more"), - ] - - operations = [ - migrations.AlterModelOptions( - name="anatomicalentity", - options={ - "ordering": ["name"], - "verbose_name_plural": "Anatomical Entities Meta", - }, - ), - migrations.AlterModelOptions( - name="anatomicalentitymeta", - options={ - "ordering": ["name"], - "verbose_name_plural": "Anatomical Entities Meta", - }, - ), - migrations.AlterField( - model_name="connectivitystatement", - name="origins", - field=models.ManyToManyField( - related_name="origins_relations", to="composer.anatomicalentity" - ), - ), - migrations.AlterField( - model_name="destination", - name="anatomical_entities", - field=models.ManyToManyField( - blank=True, - related_name="destination_connection_layers", - to="composer.anatomicalentity", - ), - ), - migrations.AlterField( - model_name="synonym", - name="anatomical_entity", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="synonyms", - to="composer.anatomicalentity", - ), - ), - migrations.AlterField( - model_name="via", - name="anatomical_entities", - field=models.ManyToManyField( - blank=True, - related_name="via_connection_layers", - to="composer.anatomicalentity", - ), - ), - ] diff --git a/backend/composer/models.py b/backend/composer/models.py index e7c29fe4..2ee07d7f 100644 --- a/backend/composer/models.py +++ b/backend/composer/models.py @@ -250,14 +250,11 @@ class AnatomicalEntityIntersection(models.Model): region = models.ForeignKey(Region, on_delete=models.CASCADE) -class AnatomicalEntity(AnatomicalEntityMeta): +class AnatomicalEntity(models.Model): + simple_entity = models.OneToOneField(AnatomicalEntityMeta, on_delete=models.CASCADE, null=True, blank=True) region_layer = models.ForeignKey(AnatomicalEntityIntersection, on_delete=models.CASCADE, null=True) - def __str__(self): - return self.name - class Meta: - ordering = ["name"] verbose_name_plural = "Anatomical Entities" @@ -653,6 +650,7 @@ class Destination(AbstractConnectionLayer): on_delete=models.CASCADE, related_name="destinations" # Overridden related_name ) + anatomical_entities = models.ManyToManyField(AnatomicalEntity, blank=True, related_name='destination_connection_layers') diff --git a/backend/composer/services/cs_ingestion/cs_ingestion_services.py b/backend/composer/services/cs_ingestion/cs_ingestion_services.py index 41d798a8..de44a33e 100644 --- a/backend/composer/services/cs_ingestion/cs_ingestion_services.py +++ b/backend/composer/services/cs_ingestion/cs_ingestion_services.py @@ -2,11 +2,12 @@ from datetime import datetime from typing import List, Dict, Optional, Tuple, Set, Any -from django.db import transaction +from django.db import transaction, IntegrityError from neurondm import orders from composer.models import AnatomicalEntity, Sentence, ConnectivityStatement, Sex, FunctionalCircuitRole, \ - ProjectionPhenotype, Phenotype, Specie, Provenance, Via, Note, User, Destination, Region, Layer + ProjectionPhenotype, Phenotype, Specie, Provenance, Via, Note, User, Destination, Region, Layer, \ + AnatomicalEntityIntersection, AnatomicalEntityMeta from .exceptions import EntityNotFoundException from .helpers import get_value_or_none, found_entity, \ ORIGINS, DESTINATIONS, VIAS, LABEL, SEX, SPECIES, ID, FORWARD_CONNECTION, SENTENCE_NUMBER, \ @@ -25,7 +26,7 @@ logger_service = LoggerService() -def ingest_statements(update_upstream=False): +def ingest_statements(update_upstream=False, update_anatomic_entities=False): statements_list = get_statements_from_neurondm(logger_service_param=logger_service) overridable_statements = get_overwritable_statements(statements_list) statements = validate_statements(overridable_statements) @@ -35,8 +36,8 @@ def ingest_statements(update_upstream=False): with transaction.atomic(): for statement in statements: sentence, _ = get_or_create_sentence(statement) - create_or_update_connectivity_statement(statement, sentence) - + create_or_update_connectivity_statement(statement, sentence, update_anatomic_entities) + # TODO: Errors in one statement should stop the ingestion? update_forward_connections(statements) except Exception as e: logger_service.add_anomaly( @@ -109,6 +110,8 @@ def validate_statements(statements: List[Dict[str, Any]]) -> List[Dict[str, Any] # Validate forward connection annotate_invalid_forward_connections(statement, statement_ids) + # TODO: Validate anatomical entities + return statements @@ -193,7 +196,8 @@ def get_or_create_sentence(statement: Dict) -> Tuple[Sentence, bool]: return sentence, created -def create_or_update_connectivity_statement(statement: Dict, sentence: Sentence) -> Tuple[ConnectivityStatement, bool]: +def create_or_update_connectivity_statement(statement: Dict, sentence: Sentence, update_anatomic_entities: bool) -> \ + Tuple[ConnectivityStatement, bool]: reference_uri = statement[ID] defaults = { "sentence": sentence, @@ -227,7 +231,7 @@ def create_or_update_connectivity_statement(statement: Dict, sentence: Sentence) else: create_invalid_note(connectivity_statement, error_message) - update_many_to_many_fields(connectivity_statement, statement) + update_many_to_many_fields(connectivity_statement, statement, update_anatomic_entities) statement[STATE] = connectivity_statement.state return connectivity_statement, created @@ -401,7 +405,8 @@ def create_invalid_note(connectivity_statement: ConnectivityStatement, note: str ) -def update_many_to_many_fields(connectivity_statement: ConnectivityStatement, statement: Dict): +def update_many_to_many_fields(connectivity_statement: ConnectivityStatement, statement: Dict, + update_anatomic_entities: bool): connectivity_statement.origins.clear() connectivity_statement.species.clear() # Notes are not cleared because they should be kept @@ -415,23 +420,25 @@ def update_many_to_many_fields(connectivity_statement: ConnectivityStatement, st for via in connectivity_statement.via_set.all(): via.delete() - add_origins(connectivity_statement, statement) - add_vias(connectivity_statement, statement) - add_destinations(connectivity_statement, statement) + add_origins(connectivity_statement, statement, update_anatomic_entities) + add_vias(connectivity_statement, statement, update_anatomic_entities) + add_destinations(connectivity_statement, statement, update_anatomic_entities) add_species(connectivity_statement, statement) add_provenances(connectivity_statement, statement) add_notes(connectivity_statement, statement) -def add_origins(connectivity_statement: ConnectivityStatement, statement: Dict): +def add_origins(connectivity_statement: ConnectivityStatement, statement: Dict, update_anatomic_entities: bool): for entity in statement[ORIGINS].anatomical_entities: try: - add_entity_to_instance(connectivity_statement, 'origins', entity) + add_entity_to_instance(connectivity_statement, 'origins', entity, update_anatomic_entities) except (EntityNotFoundException, AnatomicalEntity.DoesNotExist): - assert connectivity_statement.state == CSState.INVALID + assert connectivity_statement.state == CSState.INVALID, f"connectivity_statement {connectivity_statement} should be invalid due to entity {entity} not found but it isn't" + except IntegrityError as e: + raise e -def add_vias(connectivity_statement: ConnectivityStatement, statement: Dict): +def add_vias(connectivity_statement: ConnectivityStatement, statement: Dict, update_anatomic_entities: bool): for neurondm_via in statement[VIAS]: via_instance = Via.objects.create(connectivity_statement=connectivity_statement, type=neurondm_via.type, @@ -439,52 +446,93 @@ def add_vias(connectivity_statement: ConnectivityStatement, statement: Dict): add_entities_to_connection(via_instance, neurondm_via.anatomical_entities, neurondm_via.from_entities, - connectivity_statement) + connectivity_statement, update_anatomic_entities) -def add_destinations(connectivity_statement: ConnectivityStatement, statement: Dict): +def add_destinations(connectivity_statement: ConnectivityStatement, statement: Dict, update_anatomic_entities: bool): for neurondm_destination in statement[DESTINATIONS]: destination_instance = Destination.objects.create(connectivity_statement=connectivity_statement, type=neurondm_destination.type) add_entities_to_connection(destination_instance, neurondm_destination.anatomical_entities, neurondm_destination.from_entities, - connectivity_statement) + connectivity_statement, update_anatomic_entities) -def add_entities_to_connection(instance, anatomical_entities, from_entities, connectivity_statement): +def add_entities_to_connection(instance, anatomical_entities, from_entities, connectivity_statement, + update_anatomic_entities: bool): try: for entity in anatomical_entities: - add_entity_to_instance(instance, 'anatomical_entities', entity) + add_entity_to_instance(instance, 'anatomical_entities', entity, update_anatomic_entities) for entity in from_entities: - add_entity_to_instance(instance, 'from_entities', entity) + add_entity_to_instance(instance, 'from_entities', entity, update_anatomic_entities) except (EntityNotFoundException, AnatomicalEntity.DoesNotExist): - assert connectivity_statement.state == CSState.INVALID + assert connectivity_statement.state == CSState.INVALID, \ + f"connectivity_statement {connectivity_statement} should be invalid due to entity {entity} not found but it isn't" + except IntegrityError as e: + raise e -def add_entity_to_instance(instance, entity_field, entity): +def add_entity_to_instance(instance, entity_field, entity, update_anatomic_entities: bool): if isinstance(entity, orders.rl): - region, _ = get_or_create_complex_entity(entity.region, entity.layer) - getattr(instance, entity_field).add(region) + complex_anatomical_entity, _ = get_or_create_complex_entity(str(entity.region), str(entity.layer), + update_anatomic_entities) + getattr(instance, entity_field).add(complex_anatomical_entity) else: anatomical_entity = AnatomicalEntity.objects.filter(ontology_uri=str(entity)).first() getattr(instance, entity_field).add(anatomical_entity) -def get_or_create_complex_entity(region_uri, layer_uri): - region_entity = AnatomicalEntity.objects.filter(ontology_uri=region_uri).first() - layer_entity = AnatomicalEntity.objects.filter(ontology_uri=layer_uri).first() +def get_or_create_complex_entity(region_uri, layer_uri, update_anatomical_entities=False): + layer = Layer.objects.filter(ontology_uri=layer_uri).first() + region = Region.objects.filter(ontology_uri=region_uri).first() + + if update_anatomical_entities: + if not layer: + layer_meta = AnatomicalEntityMeta.objects.filter(ontology_uri=layer_uri).first() + if layer_meta: + layer, _ = convert_anatomical_entity_to_specific_type(layer_meta, Layer) + else: + raise EntityNotFoundException(f"Layer not found for URI: {layer_uri}") + + if not region: + region_meta = AnatomicalEntityMeta.objects.filter(ontology_uri=region_uri).first() + if region_meta: + region, _ = convert_anatomical_entity_to_specific_type(region_meta, Region) + else: + raise EntityNotFoundException(f"Region not found for URI: {region_uri}") + else: + if not layer or not region: + raise EntityNotFoundException("Required Layer or Region not found and update not permitted.") + + intersection, _ = AnatomicalEntityIntersection.objects.get_or_create(layer=layer, region=region) + anatomical_entity, created = AnatomicalEntity.objects.get_or_create(region_layer=intersection) - if not region_entity or not layer_entity: - raise EntityNotFoundException(f"Region or layer not found for URIs: {region_uri}, {layer_uri}") + return anatomical_entity, created - layer, _ = Layer.objects.get_or_create(ontology_uri=layer_uri, defaults={'name': layer_entity.name}) - region, _ = Region.objects.get_or_create(ontology_uri=region_uri, - defaults={'name': region_entity.name, 'associated_layer': layer}) - return region, layer +def convert_anatomical_entity_to_specific_type(entity_meta, target_model): + """ + Convert an AnatomicalEntityMeta instance to a more specific subclass type (Layer or Region). + Attempts to delete the original instance and create the new specific instance atomically. + """ + defaults = {'name': entity_meta.name, 'ontology_uri': entity_meta.ontology_uri} + + try: + with transaction.atomic(): + # Delete the anatomical entity in the incorrect type + entity_meta.delete() + # Create a new anatomical entity in the new specific type + specific_entity, created = target_model.objects.get_or_create( + ontology_uri=entity_meta.ontology_uri, + defaults=defaults + ) + return specific_entity, created + except IntegrityError as e: + raise IntegrityError( + f"Failed to convert AnatomicalEntityMeta to {target_model.__name__} due to integrity error: {e}") def add_notes(connectivity_statement: ConnectivityStatement, statement: Dict): @@ -522,7 +570,8 @@ def update_forward_connections(statements: List): try: forward_statement = ConnectivityStatement.objects.get(reference_uri=uri) except ConnectivityStatement.DoesNotExist: - assert connectivity_statement.state == CSState.INVALID + assert connectivity_statement.state == CSState.INVALID, \ + f"connectivity_statement {connectivity_statement} should be invalid due to forward connection {uri} not found but it isn't" continue connectivity_statement.forward_connection.add(forward_statement)