Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 09:36:17

0001 """Snapper temporal-query REST adapters (snapper-ai PLAN.md Phase 5).
0002 
0003 Thin transports over ``snapper_ai.queries``: each endpoint parses and
0004 validates its parameters, calls the generic query, and returns the typed
0005 evidence envelope's serialization unchanged — actual snap times,
0006 schema/policy versions, provenance, hashes, and observer coverage are
0007 the contract, and no adapter may present inferred continuity as fact.
0008 
0009 Read-open like the rest of the monitor's read surfaces; errors are
0010 explicit JSON, never empty results.
0011 """
0012 
0013 from django.http import JsonResponse
0014 from django.utils.dateparse import parse_datetime
0015 
0016 from snapper_ai.queries import (InvalidQuery, SnapNotFound, SnapperError,
0017                                 changes_between, component_history,
0018                                 context_around, latest, state_at)
0019 
0020 from ..snapper_resolvers import annotate_references
0021 
0022 
0023 def _parse_time(raw, label):
0024     value = parse_datetime(str(raw or '').strip())
0025     if value is None:
0026         raise InvalidQuery(
0027             f'{label} must be an ISO 8601 datetime, e.g. 2026-07-23T04:00:00Z')
0028     if value.tzinfo is None:
0029         raise InvalidQuery(f'{label} must carry an explicit timezone offset')
0030     return value
0031 
0032 
0033 def _run(query):
0034     try:
0035         result = query()
0036     except InvalidQuery as e:
0037         return JsonResponse({'error': str(e)}, status=400)
0038     except SnapNotFound as e:
0039         return JsonResponse({'error': str(e)}, status=404)
0040     except SnapperError as e:
0041         return JsonResponse({'error': str(e)}, status=500)
0042     payload = result.as_dict()
0043     return JsonResponse(payload, json_dumps_params={'default': str})
0044 
0045 
0046 def snapper_latest(request, scope):
0047     """GET /api/snapper/<scope>/latest/"""
0048     return _run(lambda: latest(scope))
0049 
0050 
0051 def snapper_state_at(request, scope):
0052     """GET /api/snapper/<scope>/state-at/?time=<ISO 8601>"""
0053     return _run(lambda: state_at(scope, _parse_time(
0054         request.GET.get('time'), 'time')))
0055 
0056 
0057 def snapper_component_history(request, scope):
0058     """GET /api/snapper/<scope>/history/?component=&start=&end=
0059     [&include_unchanged=1]"""
0060     def query():
0061         component = str(request.GET.get('component') or '').strip()
0062         if not component:
0063             raise InvalidQuery('component is required')
0064         return component_history(
0065             scope, component,
0066             _parse_time(request.GET.get('start'), 'start'),
0067             _parse_time(request.GET.get('end'), 'end'),
0068             suppress_unchanged_baselines=(
0069                 request.GET.get('include_unchanged') != '1'),
0070         )
0071     return _run(query)
0072 
0073 
0074 def snapper_changes_between(request, scope):
0075     """GET /api/snapper/<scope>/changes/?start=&end="""
0076     return _run(lambda: changes_between(
0077         scope,
0078         _parse_time(request.GET.get('start'), 'start'),
0079         _parse_time(request.GET.get('end'), 'end')))
0080 
0081 
0082 def system_status_history(request):
0083     """GET /api/system-status/history/?name=&start=&end=&limit=
0084 
0085     Read surface for the append-only health observations — the
0086     authoritative event stream behind the assessed health component
0087     (resolver swf-system-status-history).
0088     """
0089     from ..models import SystemStatusHistory
0090 
0091     rows = SystemStatusHistory.objects.order_by('-checked_at')
0092     name = (request.GET.get('name') or '').strip()
0093     if name:
0094         rows = rows.filter(name=name)
0095     raw_start = request.GET.get('start')
0096     raw_end = request.GET.get('end')
0097     try:
0098         if raw_start:
0099             rows = rows.filter(checked_at__gte=_parse_time(raw_start, 'start'))
0100         if raw_end:
0101             rows = rows.filter(checked_at__lt=_parse_time(raw_end, 'end'))
0102     except InvalidQuery as e:
0103         return JsonResponse({'error': str(e)}, status=400)
0104     try:
0105         limit = min(int(request.GET.get('limit') or 500), 2000)
0106     except ValueError:
0107         return JsonResponse({'error': 'limit must be an integer'}, status=400)
0108     if limit < 0:
0109         return JsonResponse({'error': 'limit must be non-negative'},
0110                             status=400)
0111     observations = list(rows.values(
0112         'name', 'category', 'status', 'summary', 'checked_at')[:limit])
0113     return JsonResponse({'count': len(observations),
0114                          'observations': observations},
0115                         json_dumps_params={'default': str})
0116 
0117 
0118 def snapper_context(request, scope):
0119     """GET /api/snapper/<scope>/context/?time=<ISO>[&window=seconds]
0120 
0121     State at the instant, changes in the window around it, and event
0122     references with their SWF resolver transports attached.
0123     """
0124     import math
0125 
0126     try:
0127         window = float(request.GET.get('window') or 3600)
0128     except ValueError:
0129         return JsonResponse({'error': 'window must be a number of seconds'},
0130                             status=400)
0131     if not math.isfinite(window) or window <= 0:
0132         return JsonResponse({'error': 'window must be a positive finite '
0133                                       'number of seconds'}, status=400)
0134     try:
0135         result = context_around(
0136             scope, _parse_time(request.GET.get('time'), 'time'), window)
0137     except InvalidQuery as e:
0138         return JsonResponse({'error': str(e)}, status=400)
0139     except SnapNotFound as e:
0140         return JsonResponse({'error': str(e)}, status=404)
0141     except (SnapperError, ValueError) as e:
0142         return JsonResponse({'error': str(e)}, status=500)
0143     payload = result.as_dict()
0144     payload['references'] = annotate_references(payload['references'])
0145     return JsonResponse(payload, json_dumps_params={'default': str})