Skip to content

Commit 4377118

Browse files
authored
NABat Export Annotations (#173)
* inital export commit * add regular export, swapt o csv + json files * exporting migrations, task update * basic exporting working * add celery periodic task * add celery periodic task * rename nabat export task function
1 parent 28f26db commit 4377118

File tree

18 files changed

+664
-14
lines changed

18 files changed

+664
-14
lines changed

bats_ai/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from bats_ai.core.views import (
77
ConfigurationRouter,
8+
ExportAnnotationRouter,
89
GRTSCellsRouter,
910
GuanoMetadataRouter,
1011
ProcessingTaskRouter,
@@ -38,6 +39,7 @@ def global_auth(request):
3839
api.add_router('/grts/', GRTSCellsRouter)
3940
api.add_router('/guano/', GuanoMetadataRouter)
4041
api.add_router('/recording-annotation/', RecordingAnnotationRouter)
42+
api.add_router('/export-annotation/', ExportAnnotationRouter)
4143
api.add_router('/configuration/', ConfigurationRouter)
4244
api.add_router('/processing-task/', ProcessingTaskRouter)
4345
api.add_router('/nabat/recording/', NABatRecordingRouter)

bats_ai/core/admin/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .annotations import AnnotationsAdmin
22
from .compressed_spectrogram import CompressedSpectrogramAdmin
33
from .configuration import ConfigurationAdmin
4+
from .exported_annotation import ExportedAnnotationFileAdmin
45
from .grts_cells import GRTSCellsAdmin
56
from .image import ImageAdmin
67
from .nabat.admin import (
@@ -28,6 +29,7 @@
2829
'RecordingAnnotationAdmin',
2930
'ProcessingTaskAdmin',
3031
'ConfigurationAdmin',
32+
'ExportedAnnotationFileAdmin',
3133
# NABat Models
3234
'NABatRecordingAnnotationAdmin',
3335
'NABatCompressedSpectrogramAdmin',
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from django.contrib import admin
2+
3+
from bats_ai.core.models import ExportedAnnotationFile
4+
5+
6+
@admin.register(ExportedAnnotationFile)
7+
class ExportedAnnotationFileAdmin(admin.ModelAdmin):
8+
list_display = (
9+
'id',
10+
'file',
11+
'download_url',
12+
'status',
13+
'expires_at',
14+
'created',
15+
'modified',
16+
)
17+
list_filter = ('status', 'created', 'modified', 'expires_at')
18+
search_fields = ('download_url', 'file')
19+
ordering = ('-created',)
20+
readonly_fields = ('created', 'modified')
21+
22+
fieldsets = (
23+
(None, {'fields': ('file', 'download_url', 'status', 'expires_at')}),
24+
(
25+
'Filters Applied',
26+
{
27+
'classes': ('collapse',),
28+
'fields': ('filters_applied',),
29+
},
30+
),
31+
(
32+
'Timestamps',
33+
{
34+
'fields': ('created', 'modified'),
35+
},
36+
),
37+
)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Generated by Django 4.1.13 on 2025-06-03 18:07
2+
3+
from django.db import migrations, models
4+
import django_extensions.db.fields
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
('core', '0017_configuration_default_color_scheme_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='ExportedAnnotationFile',
15+
fields=[
16+
(
17+
'id',
18+
models.BigAutoField(
19+
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
20+
),
21+
),
22+
(
23+
'created',
24+
django_extensions.db.fields.CreationDateTimeField(
25+
auto_now_add=True, verbose_name='created'
26+
),
27+
),
28+
(
29+
'modified',
30+
django_extensions.db.fields.ModificationDateTimeField(
31+
auto_now=True, verbose_name='modified'
32+
),
33+
),
34+
('file', models.FileField(upload_to='exports/')),
35+
('download_url', models.URLField(blank=True, max_length=2048, null=True)),
36+
('filters_applied', models.JSONField(blank=True, null=True)),
37+
('expires_at', models.DateTimeField()),
38+
(
39+
'status',
40+
models.CharField(
41+
choices=[
42+
('pending', 'Pending'),
43+
('complete', 'Complete'),
44+
('failed', 'Failed'),
45+
],
46+
default='pending',
47+
max_length=32,
48+
),
49+
),
50+
],
51+
options={
52+
'ordering': ['-created'],
53+
},
54+
),
55+
]

bats_ai/core/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .annotations import Annotations
22
from .compressed_spectrogram import CompressedSpectrogram
33
from .configuration import Configuration
4+
from .exported_file import ExportedAnnotationFile
45
from .grts_cells import GRTSCells
56
from .image import Image
67
from .processing_task import ProcessingTask, ProcessingTaskType
@@ -26,4 +27,5 @@
2627
'Configuration',
2728
'ProcessingTask',
2829
'ProcessingTaskType',
30+
'ExportedAnnotationFile',
2931
]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from django.db import models
2+
from django.dispatch import receiver
3+
from django_extensions.db.models import TimeStampedModel
4+
5+
6+
class ExportedAnnotationFile(TimeStampedModel):
7+
file = models.FileField(upload_to='exports/')
8+
download_url = models.URLField(blank=True, null=True, max_length=2048)
9+
filters_applied = models.JSONField(
10+
blank=True,
11+
null=True,
12+
)
13+
expires_at = models.DateTimeField()
14+
status = models.CharField(
15+
max_length=32,
16+
choices=[('pending', 'Pending'), ('complete', 'Complete'), ('failed', 'Failed')],
17+
default='pending',
18+
)
19+
20+
class Meta:
21+
ordering = ['-created']
22+
23+
24+
@receiver(models.signals.pre_delete, sender=ExportedAnnotationFile)
25+
def delete_content(sender, instance, **kwargs):
26+
if instance.file:
27+
instance.file.delete(save=False)

bats_ai/core/views/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .annotations import router as AnnotationRouter
22
from .configuration import router as ConfigurationRouter
3+
from .export_annotation import router as ExportAnnotationRouter
34
from .grts_cells import router as GRTSCellsRouter
45
from .guanometadata import router as GuanoMetadataRouter
56
from .processing_tasks import router as ProcessingTaskRouter
@@ -18,4 +19,5 @@
1819
'RecordingAnnotationRouter',
1920
'ConfigurationRouter',
2021
'ProcessingTaskRouter',
22+
'ExportAnnotationRouter',
2123
]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from datetime import datetime
2+
import logging
3+
4+
from django.http import JsonResponse
5+
from django.shortcuts import get_object_or_404
6+
from ninja import Router
7+
from pydantic import BaseModel
8+
9+
from bats_ai.core.models import ExportedAnnotationFile
10+
11+
logger = logging.getLogger(__name__)
12+
13+
router = Router()
14+
15+
16+
class ExportedAnnotationFileSchema(BaseModel):
17+
id: int
18+
status: str
19+
downloadUrl: str | None
20+
created: datetime
21+
expiresAt: datetime | None
22+
23+
class Config:
24+
orm_mode = True
25+
26+
27+
@router.get('/', response=list[ExportedAnnotationFileSchema])
28+
def list_exports(request):
29+
exports = ExportedAnnotationFile.objects.order_by('-created')
30+
return [
31+
ExportedAnnotationFileSchema(
32+
id=e.id,
33+
status=e.status,
34+
downloadUrl=e.download_url if e.status == 'complete' else None,
35+
created=e.created,
36+
expiresAt=e.expires_at,
37+
)
38+
for e in exports
39+
]
40+
41+
42+
@router.get('/{export_id}', response=ExportedAnnotationFileSchema)
43+
def get_export_status(request, export_id: int):
44+
export = get_object_or_404(ExportedAnnotationFile, pk=export_id)
45+
return ExportedAnnotationFileSchema(
46+
id=export.id,
47+
status=export.status,
48+
downloadUrl=export.download_url if export.status == 'complete' else None,
49+
created=export.created,
50+
expiresAt=export.expires_at,
51+
)
52+
53+
54+
@router.delete('/{export_id}')
55+
def delete_export(request, export_id: int):
56+
export = get_object_or_404(ExportedAnnotationFile, pk=export_id)
57+
58+
# Optional: block deleting exports still in progress
59+
if export.status not in ('complete', 'failed', 'expired'):
60+
return JsonResponse(
61+
{'error': 'Cannot delete an export that is still in progress.'},
62+
status=400,
63+
)
64+
65+
export.delete()
66+
return JsonResponse({'success': True})

bats_ai/core/views/nabat/nabat_configuration.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime
1+
from datetime import date, datetime, timedelta
22
import json
33
import logging
44
from typing import Any, Literal
@@ -8,11 +8,13 @@
88
from django.contrib.gis.geos import Point, Polygon
99
from django.db.models import Count
1010
from django.http import HttpRequest, JsonResponse
11+
from django.utils.timezone import now
1112
from ninja import Query, Router, Schema
1213
from ninja.pagination import paginate
1314

14-
from bats_ai.core.models import ProcessingTask, ProcessingTaskType
15+
from bats_ai.core.models import ExportedAnnotationFile, ProcessingTask, ProcessingTaskType
1516
from bats_ai.core.models.nabat import NABatRecording, NABatRecordingAnnotation
17+
from bats_ai.tasks.nabat.nabat_export_task import export_nabat_annotations_task
1618
from bats_ai.tasks.nabat.nabat_update_species import update_nabat_species
1719

1820
logger = logging.getLogger(__name__)
@@ -194,3 +196,25 @@ def get_stats(request: HttpRequest):
194196
'total_recordings': total_recordings,
195197
'total_annotations': total_annotations,
196198
}
199+
200+
201+
class AnnotationExportRequest(Schema):
202+
start_date: date | None = None
203+
end_date: date | None = None
204+
recording_ids: list[int] | None = None
205+
usernames: list[str] | None = None
206+
min_confidence: float | None = None
207+
max_confidence: float | None = None
208+
209+
210+
@router.post(
211+
'/export',
212+
)
213+
def export_annotations(request: HttpRequest, filters: AnnotationExportRequest):
214+
export = ExportedAnnotationFile.objects.create(
215+
filters_applied=filters.dict(),
216+
status='pending',
217+
expires_at=now() + timedelta(hours=24),
218+
)
219+
export_nabat_annotations_task.delay(filters.dict(), export.id)
220+
return export.id

bats_ai/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ class BatsAiMixin(ConfigMixin):
2626
]
2727
CELERY_RESULT_BACKEND = 'django-db'
2828

29+
CELERY_BEAT_SCHEDULE = {
30+
'delete-expired-exported-files-daily': {
31+
'task': 'bats_ai.tasks.periodic.delete_expired_exported_files',
32+
'schedule': 86400, # every 24 hours (in seconds)
33+
},
34+
}
35+
2936
@staticmethod
3037
def mutate_configuration(configuration: ComposedConfiguration) -> None:
3138
# Install local apps first, to ensure any overridden resources are found first

0 commit comments

Comments
 (0)