Skip to content

Commit c1d636f

Browse files
committed
Add v2 APIs for Project
We have added several APIs to help users easily manage Projects and objects related to each project, such as notifiers, rules, exporters, and URLs.
1 parent 008f619 commit c1d636f

29 files changed

+1330
-7
lines changed

promgen/filters.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,26 @@ class URLFilter(django_filters.rest_framework.FilterSet):
192192
choices=models.Probe.objects.values_list("module", "description").distinct(),
193193
help_text="Filter by exact probe scheme. Example: probe=http_2xx",
194194
)
195+
196+
197+
class ProjectFilterV2(django_filters.rest_framework.FilterSet):
198+
name = django_filters.CharFilter(
199+
field_name="name",
200+
lookup_expr="contains",
201+
help_text="Filter by project name containing a specific substring. Example: name=Example Project",
202+
)
203+
service = django_filters.CharFilter(
204+
field_name="service__name",
205+
lookup_expr="exact",
206+
help_text="Filter by exact service name. Example: service=Example Service",
207+
)
208+
shard = django_filters.CharFilter(
209+
field_name="shard__name",
210+
lookup_expr="exact",
211+
help_text="Filter by exact shard name. Example: shard=Example Shard",
212+
)
213+
owner = django_filters.CharFilter(
214+
field_name="owner__username",
215+
lookup_expr="exact",
216+
help_text="Filter by exact owner username. Example: owner=Example Owner",
217+
)

promgen/fixtures/testcases.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@
7070
pk: 2
7171
fields:
7272
name: another-project
73-
owner: 1
74-
service: 1
73+
owner: 2
74+
service: 2
7575
shard: 1
7676
farm: 1
7777
- model: promgen.host

promgen/rest_v2.py

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rest_framework.permissions import IsAuthenticated
1515
from rest_framework.response import Response
1616

17-
from promgen import filters, models, serializers
17+
from promgen import filters, models, serializers, signals
1818

1919
class RuleMixin:
2020
@extend_schema(
@@ -340,3 +340,194 @@ class URLViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.Gene
340340
lookup_value_regex = "[^/]+"
341341
lookup_field = "id"
342342
pagination_class = PromgenPagination
343+
344+
345+
@extend_schema_view(
346+
list=extend_schema(summary="List Projects", description="Retrieve a list of all projects."),
347+
retrieve=extend_schema(
348+
summary="Retrieve Project",
349+
description="Retrieve detailed information about a specific project.",
350+
),
351+
create=extend_schema(summary="Create Project", description="Create a new project."),
352+
update=extend_schema(summary="Update Project", description="Update an existing project."),
353+
partial_update=extend_schema(
354+
summary="Partially Update Project", description="Partially update an existing project."
355+
),
356+
destroy=extend_schema(summary="Delete Project", description="Delete an existing project."),
357+
)
358+
@extend_schema(tags=["Project"])
359+
class ProjectViewSet(NotifierMixin, RuleMixin, viewsets.ModelViewSet):
360+
queryset = models.Project.objects.prefetch_related("service", "shard", "farm")
361+
filterset_class = filters.ProjectFilterV2
362+
lookup_value_regex = "[^/]+"
363+
lookup_field = "id"
364+
pagination_class = PromgenPagination
365+
366+
def get_serializer_class(self):
367+
if self.action == "list":
368+
return serializers.ProjectRetrieveSerializer
369+
if self.action == "retrieve":
370+
return serializers.ProjectRetrieveSerializer
371+
if self.action == "create":
372+
return serializers.ProjectCreateSerializer
373+
if self.action == "update":
374+
return serializers.ProjectUpdateSerializer
375+
if self.action == "partial_update":
376+
return serializers.ProjectUpdateSerializer
377+
return serializers.ProjectRetrieveSerializer
378+
379+
@extend_schema(
380+
summary="List Exporters",
381+
description="Retrieve all exporters associated with the specified project.",
382+
responses=serializers.ExporterSerializer(many=True),
383+
)
384+
@action(detail=True, methods=["get"], pagination_class=None, filterset_class=None)
385+
def exporters(self, request, id):
386+
project = self.get_object()
387+
return Response(serializers.ExporterSerializer(project.exporter_set.all(), many=True).data)
388+
389+
@extend_schema(
390+
summary="List URLs",
391+
description="Retrieve all URLs associated with the specified project.",
392+
responses=serializers.URLSerializer(many=True),
393+
)
394+
@action(detail=True, methods=["get"], pagination_class=None, filterset_class=None)
395+
def urls(self, request, id):
396+
project = self.get_object()
397+
return Response(serializers.URLSerializer(project.url_set.all(), many=True).data)
398+
399+
@extend_schema(
400+
summary="Link Farm",
401+
description="Link a farm to the specified project.",
402+
request=serializers.LinkFarmSerializer,
403+
responses=serializers.ProjectRetrieveSerializer,
404+
)
405+
@action(detail=True, methods=["patch"], url_path="farm-link")
406+
def link_farm(self, request, id):
407+
serializer = serializers.LinkFarmSerializer(data=request.data)
408+
serializer.is_valid(raise_exception=True)
409+
project = self.get_object()
410+
farm, created = models.Farm.objects.get_or_create(
411+
name=serializer.validated_data["farm"],
412+
source=serializer.validated_data["source"],
413+
)
414+
if created:
415+
farm.refresh()
416+
project.farm = farm
417+
project.save()
418+
return Response(serializers.ProjectRetrieveSerializer(project).data)
419+
420+
@extend_schema(
421+
summary="Unlink Farm",
422+
description="Unlink the farm from the specified project.",
423+
responses=serializers.ProjectRetrieveSerializer,
424+
)
425+
@action(detail=True, methods=["patch"], url_path="farm-unlink")
426+
def unlink_farm(self, request, id):
427+
project = self.get_object()
428+
if project.farm is None:
429+
return Response(serializers.ProjectRetrieveSerializer(project).data)
430+
431+
old_farm, project.farm = project.farm, None
432+
project.save()
433+
signals.trigger_write_config.send(request)
434+
435+
if old_farm.project_set.count() == 0 and old_farm.editable is False:
436+
old_farm.delete()
437+
return Response(serializers.ProjectRetrieveSerializer(project).data)
438+
439+
@extend_schema(
440+
summary="Register URL",
441+
description="Register a new URL for the specified project.",
442+
request=serializers.RegisterURLProjectSerializer,
443+
responses={201: serializers.URLSerializer(many=True)},
444+
)
445+
@urls.mapping.post
446+
def register_url(self, request, id):
447+
serializer = serializers.RegisterURLProjectSerializer(data=request.data)
448+
serializer.is_valid(raise_exception=True)
449+
project = self.get_object()
450+
451+
models.URL.objects.get_or_create(
452+
project=project,
453+
url=serializer.validated_data["url"],
454+
probe=serializer.validated_data["probe"],
455+
)
456+
return Response(
457+
serializers.URLSerializer(project.url_set, many=True).data, status=HTTPStatus.CREATED
458+
)
459+
460+
@extend_schema(
461+
summary="Delete URL",
462+
description="Delete a URL from the specified project.",
463+
)
464+
@action(
465+
detail=True,
466+
methods=["delete"],
467+
url_path="urls/(?P<url_id>\d+)",
468+
pagination_class=None,
469+
filterset_class=None,
470+
)
471+
def delete_url(self, request, id, url_id):
472+
project = self.get_object()
473+
models.URL.objects.filter(project=project, pk=url_id).delete()
474+
return Response(status=HTTPStatus.NO_CONTENT)
475+
476+
@extend_schema(
477+
summary="Register Exporter",
478+
description="Register a new exporter for the specified project.",
479+
request=serializers.RegisterExporterProjectSerializer,
480+
responses={201: serializers.ExporterSerializer(many=True)},
481+
)
482+
@exporters.mapping.post
483+
def register_exporter(self, request, id):
484+
serializer = serializers.RegisterExporterProjectSerializer(data=request.data)
485+
serializer.is_valid(raise_exception=True)
486+
project = self.get_object()
487+
488+
attributes = {
489+
"project_id": project.id,
490+
}
491+
492+
for field in serializer.fields:
493+
value = serializer.validated_data.get(field)
494+
if value is not None:
495+
attributes[field] = value
496+
497+
models.Exporter.objects.get_or_create(**attributes)
498+
return Response(
499+
serializers.ExporterSerializer(project.exporter_set, many=True).data,
500+
status=HTTPStatus.CREATED,
501+
)
502+
503+
@extend_schema(
504+
summary="Update Exporter",
505+
description="Update an existing exporter for the specified project.",
506+
request=serializers.UpdateExporterProjectSerializer,
507+
responses=serializers.ExporterSerializer(many=True),
508+
)
509+
@action(
510+
detail=True,
511+
methods=["patch"],
512+
url_path="exporters/(?P<exporter_id>\d+)",
513+
pagination_class=None,
514+
filterset_class=None,
515+
)
516+
def update_exporter(self, request, id, exporter_id):
517+
serializer = serializers.UpdateExporterProjectSerializer(data=request.data)
518+
serializer.is_valid(raise_exception=True)
519+
project = self.get_object()
520+
exporter = models.Exporter.objects.filter(project=project, pk=exporter_id).first()
521+
exporter.enabled = serializer.validated_data.get("enabled")
522+
exporter.save()
523+
return Response(serializers.ExporterSerializer(project.exporter_set, many=True).data)
524+
525+
@extend_schema(
526+
summary="Delete Exporter",
527+
description="Delete an exporter from the specified project.",
528+
)
529+
@update_exporter.mapping.delete
530+
def delete_exporter(self, request, id, exporter_id):
531+
project = self.get_object()
532+
models.Exporter.objects.filter(project=project, pk=exporter_id).delete()
533+
return Response(status=HTTPStatus.NO_CONTENT)

promgen/serializers.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class Meta:
5353

5454
class SenderSerializer(serializers.ModelSerializer):
5555
owner = serializers.ReadOnlyField(source="owner.username")
56-
label = serializers.ReadOnlyField(source="show_value")
56+
label = serializers.CharField(source="show_value", read_only=True)
5757

5858
class Meta:
5959
model = models.Sender
@@ -251,3 +251,141 @@ class URLSerializer(serializers.ModelSerializer):
251251
class Meta:
252252
model = models.URL
253253
fields = "__all__"
254+
255+
256+
@extend_schema_field(OpenApiTypes.STR)
257+
class OwnerField(serializers.Field):
258+
def to_internal_value(self, data):
259+
if not data:
260+
return serializers.CurrentUserDefault()
261+
try:
262+
owner = User.objects.get(username=data)
263+
except User.DoesNotExist:
264+
raise serializers.ValidationError("Owner does not exist.")
265+
return owner
266+
267+
def to_representation(self, value):
268+
return value.username
269+
270+
271+
@extend_schema_field(OpenApiTypes.STR)
272+
class ServiceField(serializers.Field):
273+
def to_internal_value(self, data):
274+
try:
275+
service = models.Service.objects.get(name=data)
276+
except models.Service.DoesNotExist:
277+
raise serializers.ValidationError("Service does not exist.")
278+
return service
279+
280+
def to_representation(self, value):
281+
return value.name
282+
283+
284+
@extend_schema_field(OpenApiTypes.STR)
285+
class ShardField(serializers.Field):
286+
def to_internal_value(self, data):
287+
try:
288+
shard = models.Shard.objects.get(name=data)
289+
except models.Shard.DoesNotExist:
290+
raise serializers.ValidationError("Shard does not exist.")
291+
return shard
292+
293+
def to_representation(self, value):
294+
return value.name
295+
296+
297+
@extend_schema_field(OpenApiTypes.STR)
298+
class FarmField(serializers.Field):
299+
def to_internal_value(self, data):
300+
try:
301+
farm = models.Farm.objects.get(name=data)
302+
except models.Farm.DoesNotExist:
303+
raise serializers.ValidationError("Farm does not exist.")
304+
return farm
305+
306+
def to_representation(self, value):
307+
return value.name
308+
309+
310+
class ProjectRetrieveSerializer(serializers.ModelSerializer):
311+
owner = serializers.ReadOnlyField(source="owner.username")
312+
service = serializers.ReadOnlyField(source="service.name")
313+
shard = serializers.ReadOnlyField(source="shard.name")
314+
farm = serializers.ReadOnlyField(source="farm.name")
315+
316+
class Meta:
317+
model = models.Project
318+
fields = "__all__"
319+
320+
321+
class ProjectCreateSerializer(serializers.ModelSerializer):
322+
owner = OwnerField(required=False, default=serializers.CurrentUserDefault())
323+
service = ServiceField()
324+
shard = ShardField()
325+
farm = FarmField(required=False)
326+
327+
class Meta:
328+
model = models.Project
329+
fields = "__all__"
330+
331+
332+
class ProjectUpdateSerializer(serializers.ModelSerializer):
333+
owner = OwnerField(required=False)
334+
service = ServiceField(required=False, read_only=True)
335+
shard = ShardField(required=False)
336+
farm = serializers.ReadOnlyField(source="farm.name")
337+
name = serializers.CharField(required=False)
338+
339+
class Meta:
340+
model = models.Project
341+
fields = "__all__"
342+
343+
344+
class LinkFarmSerializer(serializers.Serializer):
345+
farm = serializers.CharField()
346+
source = serializers.ChoiceField(choices=[name for name, _ in models.Farm.driver_set()])
347+
348+
349+
@extend_schema_field(OpenApiTypes.STR)
350+
class ProbeField(serializers.Field):
351+
def to_internal_value(self, data):
352+
try:
353+
probe = models.Probe.objects.get(module=data)
354+
except models.Probe.DoesNotExist:
355+
raise serializers.ValidationError("Probe does not exist.")
356+
return probe
357+
358+
def to_representation(self, value):
359+
return value.module
360+
361+
362+
class RegisterURLProjectSerializer(serializers.Serializer):
363+
url = serializers.CharField()
364+
probe = ProbeField()
365+
366+
367+
class RegisterExporterProjectSerializer(serializers.Serializer):
368+
job = serializers.CharField()
369+
port = serializers.IntegerField()
370+
path = serializers.CharField()
371+
scheme = serializers.ChoiceField(choices=["http", "https"])
372+
enabled = serializers.BooleanField(required=False)
373+
374+
375+
class UpdateExporterProjectSerializer(serializers.Serializer):
376+
enabled = serializers.BooleanField()
377+
378+
379+
class DeleteExporterProjectSerializer(serializers.Serializer):
380+
job = serializers.CharField()
381+
port = serializers.IntegerField()
382+
path = serializers.CharField()
383+
scheme = serializers.CharField()
384+
385+
386+
class RegisterNotifierSerializer(serializers.Serializer):
387+
sender = serializers.ChoiceField(choices=[name for name, _ in models.Sender.driver_set()])
388+
value = serializers.CharField()
389+
alias = serializers.CharField(required=False)
390+
enabled = serializers.BooleanField(required=False, default=True)
391+
filters = FilterSerializer(many=True, required=False)

0 commit comments

Comments
 (0)