Canonical API 'skeleton': urls & views

this gives me something to look at and work with, despite
being wildly incomplete

See #146
This commit is contained in:
Klaas van Schelven
2025-09-08 22:03:42 +02:00
parent 9a36426689
commit 4c2c26743e
13 changed files with 215 additions and 0 deletions

View File

@@ -66,8 +66,21 @@ INSTALLED_APPS = [
'tailwind', # As currently set up, this is also needed in production (templatetags)
'admin_auto_filters',
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # from the tutorial
'PAGE_SIZE': 10,
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
],
}
BUGSINK_APPS = [
'bsmain',
'phonehome',

View File

@@ -5,6 +5,8 @@ from django.urls import include, path
from django.contrib.auth import views as auth_views
from django.views.generic import RedirectView, TemplateView
from rest_framework import routers
from alerts.views import debug_email as debug_alerts_email
from users.views import debug_email as debug_users_email
from teams.views import debug_email as debug_teams_email
@@ -14,6 +16,12 @@ from ingest.views import download_envelope
from files.views import chunk_upload, artifact_bundle_assemble, api_root, api_catch_all
from bugsink.decorators import login_exempt
from events.api_views import EventViewSet
from issues.api_views import IssueViewSet, GroupingViewSet
from projects.api_views import ProjectViewSet
from releases.api_views import ReleaseViewSet
from teams.api_views import TeamViewSet
from .views import home, trigger_error, favicon, settings_view, silence_email_system_warning, counts, health_check_ready
from .debug_views import csrf_debug
@@ -23,6 +31,15 @@ admin.site.site_title = get_settings().SITE_TITLE
admin.site.index_title = "Admin" # everyone calls this the "admin" anyway. Let's set the title accordingly.
api_router = routers.DefaultRouter()
api_router.register(r'events', EventViewSet)
api_router.register(r'issues', IssueViewSet)
api_router.register(r'groupings', GroupingViewSet)
api_router.register(r'projects', ProjectViewSet)
api_router.register(r'releases', ReleaseViewSet)
api_router.register(r'teams', TeamViewSet)
urlpatterns = [
path('', home, name='home'),
@@ -43,6 +60,8 @@ urlpatterns = [
# many user-related views are directly exposed above (/accounts/), the rest is here:
path("users/", include("users.urls")),
path("api/canonical/0/", include(api_router.urls)),
# these are sentry-cli endpoint for uploading; they're unrelated to e.g. the ingestion API.
# the /api/0/ is just a hard prefix (for the ingest API, that position indicates the project id, but here it's just
# a prefix)

11
events/api_views.py Normal file
View File

@@ -0,0 +1,11 @@
from rest_framework import viewsets
from .models import Event
from .serializers import EventSerializer
class EventViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Event.objects.all().order_by('digest_order')
serializer_class = EventSerializer
# TODO: the idea of required filter-fields when listing.

25
events/serializers.py Normal file
View File

@@ -0,0 +1,25 @@
from rest_framework import serializers
from .models import Event
class EventSerializer(serializers.ModelSerializer):
class Meta:
model = Event
# TODO better wording:
# This is the first attempt at getting the list of fields right. My belief is: this is a nice minimal list.
# it _does_ contain `data`, which is typically quite "fat", but I'd say that's the most useful field to have.
# and when you're actually in the business of looking at a specific event, you want to see the data.
fields = [
"id",
"ingested_at",
"digested_at",
"issue",
"grouping",
"event_id",
"project",
"data", # TODO fetch from disk if so-configured
"timestamp",
"digest_order",
]

16
issues/api_views.py Normal file
View File

@@ -0,0 +1,16 @@
from rest_framework import viewsets
from .models import Issue, Grouping
from .serializers import IssueSerializer, GroupingSerializer
class IssueViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Issue.objects.all().order_by('digest_order') # TBD
serializer_class = IssueSerializer
class GroupingViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Grouping.objects.all().order_by('grouping_key') # TBD
serializer_class = GroupingSerializer
# TODO: the idea of required filter-fields when listing.

45
issues/serializers.py Normal file
View File

@@ -0,0 +1,45 @@
from rest_framework import serializers
from .models import Issue, Grouping
class IssueSerializer(serializers.ModelSerializer):
class Meta:
model = Issue
# TODO better wording:
# This is the first attempt at getting the list of fields right. My belief is: this is a nice minimal list.
# it _does_ contain `data`, which is typically quite "fat", but I'd say that's the most useful field to have.
# and when you're actually in the business of looking at a specific event, you want to see the data.
fields = [
"id",
"project",
"is_deleted",
"digest_order",
"last_seen",
"first_seen",
"digested_event_count",
"stored_event_count",
"calculated_type",
"calculated_value",
"transaction",
# "last_frame_filename",
# "last_frame_module",
# "last_frame_function",
"is_resolved",
"is_resolved_by_next_release",
# "fixed_at", too "raw"? i.e. too implementation-tied?
# "events_at", too "raw"? i.e. too implementation-tied?
"is_muted",
# "unmute_on_volume_based_conditions", too "raw"? i.e. too implementation-tied?
]
class GroupingSerializer(serializers.ModelSerializer):
class Meta:
model = Grouping
fields = [
"project",
"grouping_key",
"issue",
]

9
projects/api_views.py Normal file
View File

@@ -0,0 +1,9 @@
from rest_framework import viewsets
from .models import Project
from .serializers import ProjectSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all().order_by('id')
serializer_class = ProjectSerializer

24
projects/serializers.py Normal file
View File

@@ -0,0 +1,24 @@
from rest_framework import serializers
from .models import Project
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = [
"id",
"team",
"name",
"slug",
"is_deleted",
"sentry_key", # or just: "dsn"
# users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, through="ProjectMembership")
"digested_event_count",
"stored_event_count",
"alert_on_new_issue",
"alert_on_regression",
"alert_on_unmute",
"visibility",
"retention_max_event_count",
]

11
releases/api_views.py Normal file
View File

@@ -0,0 +1,11 @@
from rest_framework import viewsets
from .models import Release
from .serializers import ReleaseSerializer
class ReleaseViewSet(viewsets.ModelViewSet):
queryset = Release.objects.all().order_by('sort_epoch')
serializer_class = ReleaseSerializer
# TODO: the idea of required filter-fields when listing; in particular: project is required.

19
releases/serializers.py Normal file
View File

@@ -0,0 +1,19 @@
from rest_framework import serializers
from .models import Release
class ReleaseSerializer(serializers.ModelSerializer):
class Meta:
model = Release
# TODO: distinguish read vs write fields
fields = [
"id",
"project",
"version",
"date_released",
"semver",
"is_semver",
"sort_epoch",
]

View File

@@ -16,3 +16,4 @@ user-agents==2.2.*
fastjsonschema==2.21.*
verbose_csrf_middleware==1.0.*
ecma426>=0.2.0
djangorestframework==3.16.*

9
teams/api_views.py Normal file
View File

@@ -0,0 +1,9 @@
from rest_framework import viewsets
from .models import Team
from .serializers import TeamSerializer
class TeamViewSet(viewsets.ReadOnlyModelViewSet): # create? then we need a way to deal with visibility
queryset = Team.objects.all().order_by('name') # ordering: TBD
serializer_class = TeamSerializer

13
teams/serializers.py Normal file
View File

@@ -0,0 +1,13 @@
from rest_framework import serializers
from .models import Team
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = [
"id",
"name",
# "visibility",
]