Skip to content

Commit 78515c1

Browse files
hc-sousacursoragent
andcommitted
feat(seismic): EMSC sync, v3 events API, and felt reports
Poll seismicportal FDSN hourly, expose list/detail/felt endpoints, enable the module on sao-miguel, and add API tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 27798b2 commit 78515c1

13 files changed

Lines changed: 682 additions & 0 deletions

src/seismic/api_v3.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Seismic v3 API."""
2+
3+
from __future__ import annotations
4+
5+
from rest_framework import status
6+
from rest_framework.decorators import api_view, permission_classes
7+
from rest_framework.permissions import AllowAny
8+
from rest_framework.request import Request
9+
from rest_framework.response import Response
10+
11+
from consent.services import hash_session_id
12+
from seismic.models import SeismicEvent
13+
from seismic.serializers import FeltReportSerializer
14+
from seismic.services import get_event, list_events, submit_felt_report
15+
from tenancy.services import for_island
16+
17+
18+
def _require_island(request: Request) -> Response | None:
19+
if request.island is None:
20+
return Response(
21+
{'error': {'code': 'island_required', 'message': 'Island context required'}},
22+
status=status.HTTP_400_BAD_REQUEST,
23+
)
24+
return None
25+
26+
27+
@api_view(['GET'])
28+
@permission_classes([AllowAny])
29+
def seismic_events_view(request: Request) -> Response:
30+
err = _require_island(request)
31+
if err:
32+
return err
33+
34+
min_mag_raw = request.GET.get('min_magnitude', '').strip()
35+
min_magnitude = None
36+
if min_mag_raw:
37+
try:
38+
min_magnitude = float(min_mag_raw)
39+
except ValueError:
40+
return Response(
41+
{'error': {'code': 'invalid_min_magnitude', 'message': 'min_magnitude must be a number'}},
42+
status=status.HTTP_400_BAD_REQUEST,
43+
)
44+
45+
limit_raw = request.GET.get('limit', '50').strip()
46+
try:
47+
limit = int(limit_raw)
48+
except ValueError:
49+
limit = 50
50+
51+
with for_island(request.island):
52+
events = list_events(min_magnitude=min_magnitude, limit=limit)
53+
return Response({'events': events})
54+
55+
56+
@api_view(['GET'])
57+
@permission_classes([AllowAny])
58+
def seismic_event_detail_view(request: Request, event_id: int) -> Response:
59+
err = _require_island(request)
60+
if err:
61+
return err
62+
63+
with for_island(request.island):
64+
payload = get_event(event_id)
65+
if payload is None:
66+
return Response(
67+
{'error': {'code': 'not_found', 'message': 'Event not found'}},
68+
status=status.HTTP_404_NOT_FOUND,
69+
)
70+
return Response(payload)
71+
72+
73+
@api_view(['POST'])
74+
@permission_classes([AllowAny])
75+
def seismic_event_felt_view(request: Request, event_id: int) -> Response:
76+
err = _require_island(request)
77+
if err:
78+
return err
79+
80+
serializer = FeltReportSerializer(data=request.data)
81+
serializer.is_valid(raise_exception=True)
82+
data = serializer.validated_data
83+
session_id = data['session_id'].strip()
84+
if not session_id:
85+
return Response(
86+
{'error': {'code': 'session_required', 'message': 'session_id is required'}},
87+
status=status.HTTP_400_BAD_REQUEST,
88+
)
89+
90+
session_hash = hash_session_id(session_id, request.island.key)
91+
92+
with for_island(request.island):
93+
if not SeismicEvent.objects.filter(id=event_id).exists():
94+
return Response(
95+
{'error': {'code': 'not_found', 'message': 'Event not found'}},
96+
status=status.HTTP_404_NOT_FOUND,
97+
)
98+
payload, created = submit_felt_report(
99+
event_id=event_id,
100+
session_hash=session_hash,
101+
intensity=data['intensity'],
102+
latitude=data.get('latitude'),
103+
longitude=data.get('longitude'),
104+
)
105+
106+
return Response(payload, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Register hourly EMSC sync task."""
2+
3+
from django.db import migrations
4+
5+
6+
def _hourly_schedule(apps):
7+
CrontabSchedule = apps.get_model('django_celery_beat', 'CrontabSchedule')
8+
schedule, _ = CrontabSchedule.objects.get_or_create(
9+
minute='0',
10+
hour='*',
11+
day_of_week='*',
12+
day_of_month='*',
13+
month_of_year='*',
14+
defaults={'timezone': 'Atlantic/Azores'},
15+
)
16+
if schedule.timezone != 'Atlantic/Azores':
17+
schedule.timezone = 'Atlantic/Azores'
18+
schedule.save(update_fields=['timezone'])
19+
return schedule
20+
21+
22+
def register_sync_task(apps, schema_editor):
23+
PeriodicTask = apps.get_model('django_celery_beat', 'PeriodicTask')
24+
hourly = _hourly_schedule(apps)
25+
PeriodicTask.objects.update_or_create(
26+
name='seismic.sync_events',
27+
defaults={
28+
'task': 'seismic.sync_events',
29+
'crontab': hourly,
30+
'enabled': True,
31+
},
32+
)
33+
34+
35+
class Migration(migrations.Migration):
36+
dependencies = [
37+
('seismic', '0001_initial'),
38+
('django_celery_beat', '__latest__'),
39+
]
40+
41+
operations = [
42+
migrations.RunPython(register_sync_task, migrations.RunPython.noop),
43+
]

src/seismic/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Seismic v3 request serializers."""
2+
3+
from __future__ import annotations
4+
5+
from rest_framework import serializers
6+
7+
8+
class FeltReportSerializer(serializers.Serializer):
9+
session_id = serializers.CharField(max_length=128)
10+
intensity = serializers.IntegerField(min_value=1, max_value=12)
11+
latitude = serializers.FloatField(required=False, allow_null=True)
12+
longitude = serializers.FloatField(required=False, allow_null=True)

0 commit comments

Comments
 (0)