File indexing completed on 2026-08-12 09:36:17
0001 """Snapper episode REST adapters (snapper-ai docs/EPISODES.md).
0002
0003 Thin transports over ``snapper_ai.episodes``. The write endpoints are
0004 token-authenticated — the episode builder agent posts open, append,
0005 and close with its builder identity. The read endpoints are read-open
0006 like the monitor's other read surfaces; errors are explicit JSON.
0007 """
0008
0009 from dataclasses import asdict
0010
0011 from django.http import JsonResponse
0012 from rest_framework.authentication import (SessionAuthentication,
0013 TokenAuthentication)
0014 from rest_framework.decorators import (api_view, authentication_classes,
0015 permission_classes)
0016 from rest_framework.permissions import IsAuthenticated
0017
0018 from snapper_ai.episodes import (BuilderNotAuthorized, EpisodeClosed,
0019 EpisodeNotFound, InvalidEpisode,
0020 append_events, close_episode,
0021 episode_record, list_episodes,
0022 open_episode)
0023
0024
0025 def _run_write(call):
0026 try:
0027 update = call()
0028 except InvalidEpisode as e:
0029 return JsonResponse({'error': str(e)}, status=400)
0030 except EpisodeNotFound as e:
0031 return JsonResponse({'error': str(e)}, status=404)
0032 except EpisodeClosed as e:
0033 return JsonResponse({'error': str(e)}, status=409)
0034 except BuilderNotAuthorized as e:
0035 return JsonResponse({'error': str(e)}, status=403)
0036 return JsonResponse(asdict(update))
0037
0038
0039 @api_view(['POST'])
0040 @authentication_classes([TokenAuthentication, SessionAuthentication])
0041 @permission_classes([IsAuthenticated])
0042 def episodes_open(request):
0043 """POST /api/snapper/episodes/open/"""
0044 body = request.data
0045 return _run_write(lambda: open_episode(
0046 scope=body.get('scope'),
0047 episode_id=body.get('episode_id'),
0048 builder_identity=body.get('builder_identity'),
0049 started_at=body.get('started_at'),
0050 label=body.get('label') or '',
0051 kind=body.get('kind') or '',
0052 summary=body.get('summary'),
0053 ))
0054
0055
0056 @api_view(['POST'])
0057 @authentication_classes([TokenAuthentication, SessionAuthentication])
0058 @permission_classes([IsAuthenticated])
0059 def episodes_append(request):
0060 """POST /api/snapper/episodes/append/"""
0061 body = request.data
0062 return _run_write(lambda: append_events(
0063 scope=body.get('scope'),
0064 episode_id=body.get('episode_id'),
0065 builder_identity=body.get('builder_identity'),
0066 events=body.get('events'),
0067 participants=body.get('participants'),
0068 ))
0069
0070
0071 @api_view(['POST'])
0072 @authentication_classes([TokenAuthentication, SessionAuthentication])
0073 @permission_classes([IsAuthenticated])
0074 def episodes_close(request):
0075 """POST /api/snapper/episodes/close/"""
0076 body = request.data
0077 return _run_write(lambda: close_episode(
0078 scope=body.get('scope'),
0079 episode_id=body.get('episode_id'),
0080 builder_identity=body.get('builder_identity'),
0081 ended_at=body.get('ended_at'),
0082 summary=body.get('summary'),
0083 ))
0084
0085
0086 def episodes_list_view(request, scope):
0087 """GET /api/snapper/<scope>/episodes/?limit=N"""
0088 try:
0089 limit = int(request.GET.get('limit') or 50)
0090 except ValueError:
0091 return JsonResponse({'error': 'limit must be an integer'}, status=400)
0092 try:
0093 episodes = list_episodes(scope, limit=limit)
0094 except InvalidEpisode as e:
0095 return JsonResponse({'error': str(e)}, status=400)
0096 return JsonResponse({'count': len(episodes), 'episodes': episodes},
0097 json_dumps_params={'default': str})
0098
0099
0100 def episode_detail_view(request, scope, episode_id):
0101 """GET /api/snapper/<scope>/episodes/<episode_id>/"""
0102 try:
0103 record = episode_record(scope, episode_id)
0104 except InvalidEpisode as e:
0105 return JsonResponse({'error': str(e)}, status=400)
0106 except EpisodeNotFound as e:
0107 return JsonResponse({'error': str(e)}, status=404)
0108 return JsonResponse(record, json_dumps_params={'default': str})