diff --git a/docker/requirements.txt b/docker/requirements.txt index 5731b5760..9f7e8ffed 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -56,6 +56,8 @@ django-filter==23.2 # via promgen (pyproject.toml) djangorestframework==3.14.0 # via promgen (pyproject.toml) +drf-spectacular==0.28.0 + # via promgen (pyproject.toml) gunicorn==22.0.0 # via -r docker/requirements.in idna==3.7 diff --git a/promgen/admin.py b/promgen/admin.py index 1d90c12fb..5ad7c8f42 100644 --- a/promgen/admin.py +++ b/promgen/admin.py @@ -172,3 +172,8 @@ def has_add_permission(self, request, obj=None): def has_change_permission(self, request, obj=None): return False + + +@admin.register(models.SiteConfiguration) +class SiteConfigurationAdmin(admin.ModelAdmin): + list_display = ("key", "value") diff --git a/promgen/filters.py b/promgen/filters.py index d4b991132..b0be77464 100644 --- a/promgen/filters.py +++ b/promgen/filters.py @@ -1,4 +1,7 @@ import django_filters +from django.contrib.contenttypes.models import ContentType + +from promgen import models class ShardFilter(django_filters.rest_framework.FilterSet): @@ -22,5 +25,207 @@ class RuleFilter(django_filters.rest_framework.FilterSet): class FarmFilter(django_filters.rest_framework.FilterSet): - name = django_filters.CharFilter(field_name="name", lookup_expr="contains") - source = django_filters.CharFilter(field_name="source", lookup_expr="exact") + name = django_filters.CharFilter( + field_name="name", + lookup_expr="contains", + help_text="Filter by farm name containing a specific substring. Example: name=Example Farm", + ) + source = django_filters.ChoiceFilter( + field_name="source", + choices=[(name, name) for name, _ in models.Farm.driver_set()], + lookup_expr="exact", + help_text="Filter by exact source name. Example: source=promgen", + ) + + +class UserFilter(django_filters.rest_framework.FilterSet): + username = django_filters.CharFilter( + field_name="username", + lookup_expr="contains", + help_text="Filter by username containing a specific substring. Example: username=Example Username", + ) + email = django_filters.CharFilter( + field_name="email", + lookup_expr="contains", + help_text="Filter by email containing a specific substring. Example: email=example@example.com", + ) + + +def filter_content_type(queryset, name, value): + try: + content_type_id = ContentType.objects.get(model=value, app_label="promgen").id + return queryset.filter(content_type_id=content_type_id) + except ContentType.DoesNotExist: + return queryset.none() + + +class AuditFilter(django_filters.rest_framework.FilterSet): + object_id = django_filters.NumberFilter( + field_name="object_id", + lookup_expr="exact", + help_text="Filter by exact object ID. Example: object_id=123", + ) + content_type = django_filters.ChoiceFilter( + field_name="content_type", + choices=[ + ("service", "Service"), + ("project", "Project"), + ("rule", "Rule"), + ("sender", "Notifier"), + ("exporter", "Exporter"), + ("url", "URL"), + ("farm", "Farm"), + ("host", "Host"), + ], + method=filter_content_type, + help_text="Filter by content type model name. Example: content_type=service", + ) + user = django_filters.CharFilter( + field_name="user__username", + lookup_expr="exact", + help_text="Filter by exact owner username. Example: owner=Example Owner", + ) + + +class NotifierFilter(django_filters.rest_framework.FilterSet): + sender = django_filters.ChoiceFilter( + field_name="sender", + choices=[(module_name, module_name) for module_name, _ in models.Sender.driver_set()], + help_text="Filter by sender type. Example: sender=promgen.notification.email", + ) + value = django_filters.CharFilter( + field_name="value", + lookup_expr="contains", + help_text="Filter by value containing a specific substring. Example: value=demo@example.com", + ) + object_id = django_filters.NumberFilter( + field_name="object_id", + lookup_expr="exact", + help_text="Filter by exact object ID. Example: object_id=123", + ) + content_type = django_filters.ChoiceFilter( + field_name="content_type", + choices=[ + ("service", "Service"), + ("project", "Project"), + ], + method=filter_content_type, + help_text="Filter by content type model name. Example: content_type=service", + ) + owner = django_filters.CharFilter( + field_name="owner__username", + lookup_expr="exact", + help_text="Filter by exact owner username. Example: owner=Example Owner", + ) + + +class RuleFilterV2(django_filters.rest_framework.FilterSet): + name = django_filters.CharFilter( + field_name="name", + lookup_expr="contains", + help_text="Filter by rule name containing a specific substring. Example: name=Example Rule", + ) + parent = django_filters.CharFilter( + field_name="parent__name", + lookup_expr="contains", + help_text="Filter by parent rule name containing a specific substring. Example: parent=Example Parent", + ) + enabled = django_filters.BooleanFilter( + field_name="enabled", + help_text="Filter by enabled status (true or false). Example: enabled=true", + ) + object_id = django_filters.NumberFilter( + field_name="object_id", + lookup_expr="exact", + help_text="Filter by exact object ID. Example: object_id=123", + ) + content_type = django_filters.ChoiceFilter( + field_name="content_type", + choices=[ + ("service", "Service"), + ("project", "Project"), + ("site", "Site"), + ], + method=filter_content_type, + help_text="Filter by content type model name. Example: content_type=service", + ) + + +class ExporterFilter(django_filters.rest_framework.FilterSet): + project = django_filters.CharFilter( + field_name="project__name", + lookup_expr="contains", + help_text="Filter by project name containing a specific substring. Example: project=Example Project", + ) + job = django_filters.CharFilter( + field_name="job", + lookup_expr="contains", + help_text="Filter by job name containing a specific substring. Example: job=prometheus", + ) + path = django_filters.CharFilter( + field_name="path", + lookup_expr="contains", + help_text="Filter by path containing a specific substring. Example: path=/metrics", + ) + scheme = django_filters.ChoiceFilter( + field_name="scheme", + choices=[ + ("http", "HTTP"), + ("https", "HTTPS"), + ], + lookup_expr="exact", + help_text="Filter by exact scheme. Example: scheme=http", + ) + enabled = django_filters.BooleanFilter( + field_name="enabled", + help_text="Filter by enabled status (true or false). Example: enabled=true", + ) + + +class URLFilter(django_filters.rest_framework.FilterSet): + project = django_filters.CharFilter( + field_name="project__name", + lookup_expr="contains", + help_text="Filter by project name containing a specific substring. Example: project=Example Project", + ) + probe = django_filters.ChoiceFilter( + field_name="probe__module", + choices=models.Probe.objects.values_list("module", "description").distinct(), + help_text="Filter by exact probe scheme. Example: probe=http_2xx", + ) + + +class ProjectFilterV2(django_filters.rest_framework.FilterSet): + name = django_filters.CharFilter( + field_name="name", + lookup_expr="contains", + help_text="Filter by project name containing a specific substring. Example: name=Example Project", + ) + service = django_filters.CharFilter( + field_name="service__name", + lookup_expr="exact", + help_text="Filter by exact service name. Example: service=Example Service", + ) + shard = django_filters.CharFilter( + field_name="shard__name", + lookup_expr="exact", + help_text="Filter by exact shard name. Example: shard=Example Shard", + ) + owner = django_filters.CharFilter( + field_name="owner__username", + lookup_expr="exact", + help_text="Filter by exact owner username. Example: owner=Example Owner", + ) + + +class ServiceFilterV2(django_filters.rest_framework.FilterSet): + name = django_filters.CharFilter( + field_name="name", + lookup_expr="contains", + help_text="Filter by service name containing a specific substring. Example: name=Example Service", + ) + owner = django_filters.CharFilter( + field_name="owner__username", + lookup_expr="exact", + help_text="Filter by exact owner username. Example: owner=Example Owner", + ) diff --git a/promgen/fixtures/testcases.yaml b/promgen/fixtures/testcases.yaml index 39543c9d8..cd567d8a0 100644 --- a/promgen/fixtures/testcases.yaml +++ b/promgen/fixtures/testcases.yaml @@ -19,6 +19,18 @@ password: demo email: demo@example.com is_active: true +- model: authtoken.token + pk: 1 + fields: + user: 1 + key: admin_token + created: 2024-03-18T00:00:00Z +- model: authtoken.token + pk: 2 + fields: + user: 2 + key: demo_token + created: 2024-03-18T00:00:00Z - model: promgen.shard pk: 1 fields: @@ -26,6 +38,13 @@ url: http://prometheus.example.com proxy: true enabled: true +- model: promgen.shard + pk: 2 + fields: + name: other-shard + url: http://prometheus-002.example.com + proxy: false + enabled: false - model: promgen.service pk: 1 fields: @@ -35,12 +54,17 @@ pk: 2 fields: name: other-service - owner: 1 + owner: 2 - model: promgen.farm pk: 1 fields: name: test-farm source: promgen +- model: promgen.farm + pk: 2 + fields: + name: other-farm + source: external - model: promgen.project pk: 1 fields: @@ -53,30 +77,110 @@ pk: 2 fields: name: another-project - owner: 1 - service: 1 + owner: 2 + service: 2 shard: 1 farm: 1 +- model: promgen.host + pk: 1 + fields: + name: example.com + farm: 1 - model: promgen.exporter pk: 1 fields: project: 1 job: node port: 9100 + path: /metrics - model: promgen.exporter pk: 2 fields: project: 2 - job: node + job: nginx port: 9100 enabled: false + scheme: https - model: promgen.probe pk: 1 fields: module: fixture_test +- model: promgen.probe + pk: 2 + fields: + module: http_2xx - model: promgen.url pk: 1 fields: project: 1 probe: 1 url: probe.example.com +- model: promgen.url + pk: 2 + fields: + project: 2 + probe: 2 + url: probe-2.example.com +- model: promgen.audit + pk: 1 + fields: + content_type: ["promgen", "service"] + object_id: 1 + body: "Created test-service" + user: 1 + created: 2024-03-19T00:00:00Z +- model: promgen.audit + pk: 2 + fields: + content_type: ["promgen", "project"] + object_id: 1 + body: "Updated test-project" + user: 2 + created: 2024-03-19T01:00:00Z +- model: promgen.sender + pk: 1 + fields: + content_type: ["promgen", "service"] + object_id: 1 + sender: promgen.notification.email + value: "email@example.com" + owner: 1 +- model: promgen.sender + pk: 2 + fields: + content_type: ["promgen", "project"] + object_id: 1 + sender: promgen.notification.slack + value: "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + owner: 2 +- model: promgen.filter + pk: 1 + fields: + sender: 1 + name: "severity" + value: "critical" +- model: promgen.rule + pk: 1 + fields: + content_type: ["promgen", "service"] + object_id: 1 + name: "Test Rule" + clause: "up == 0" + duration: 5m + enabled: true + description: "Test Rule Description" + labels: { "severity": "critical" } + annotations: { "rule": "http://promgen.example.com/rule/1" } +- model: promgen.rule + pk: 2 + fields: + content_type: ["promgen", "project"] + object_id: 1 + name: "Test Rule 2" + clause: "up != 0" + duration: 1m + enabled: false + description: "Test Rule 2 Description" + labels: { "severity": "warning" } + annotations: { "rule": "http://promgen.example.com/rule/2" } + parent: 1 \ No newline at end of file diff --git a/promgen/middleware.py b/promgen/middleware.py index 3c7e4c1a7..b7984bfb0 100644 --- a/promgen/middleware.py +++ b/promgen/middleware.py @@ -17,13 +17,18 @@ caching system to set a key and then triggering the actual event from middleware """ +import json import logging +import uuid +from http import HTTPStatus from threading import local from django.contrib import messages from django.db.models import prefetch_related_objects +from django.http import JsonResponse +from rest_framework import views, exceptions -from promgen import models +from promgen import models, settings from promgen.signals import trigger_write_config, trigger_write_rules, trigger_write_urls logger = logging.getLogger(__name__) @@ -50,8 +55,46 @@ def __call__(self, request): if request.user.is_authenticated: _user.value = request.user + # Log all requests to our v2 API endpoints + if settings.ENABLE_API_LOGGING and request.path.startswith("/rest/v2/"): + try: + # Generate a trace ID for each request + trace_id = str(uuid.uuid4()) + request.trace_id = trace_id + # Log the IP address of the request + ip_address = request.META.get("REMOTE_ADDR") + logger.info(f"[Trace ID: {trace_id}] IP Address: {ip_address}") + # Log the user if authenticated + if request.user.is_authenticated: + logger.info(f"[Trace ID: {trace_id}] User: {request.user.username}") + # Log the request details + logger.info( + f"[Trace ID: {request.trace_id}] Request: {request.method} {request.get_full_path()} - body size: {len(request.body) if request.body else 0} bytes" + ) + if request.body and request.headers["Content-Type"] == "application/json": + # Only log first 512 characters of the request body to avoid flooding the logs + logger.info( + f"[Trace ID: {request.trace_id}] Request body: {json.dumps(json.loads(request.body))[:512]}" + ) + except Exception as e: + logger.exception( + f"[Trace ID: {request.trace_id}] An error occurred when parsing request: {str(e)}" + ) + response = self.get_response(request) + # Log all responses to our v2 API endpoints + if settings.ENABLE_API_LOGGING and request.path.startswith("/rest/v2/"): + try: + # Log the response details + logger.info( + f"[Trace ID: {request.trace_id}] Response status: {response.status_code} - content size: {len(response.content)} bytes" + ) + except Exception as e: + logger.exception( + f"[Trace ID: {request.trace_id}] An error occurred when logging response: {str(e)}" + ) + triggers = { "Config": trigger_write_config.send, "Rules": trigger_write_rules.send, @@ -67,3 +110,18 @@ def __call__(self, request): def get_current_user(): return getattr(_user, "value", None) + + +def custom_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = views.exception_handler(exc, context) + + if response is None: + # If an exception is raised that we don't handle, we will return a 500 error + # with the exception message. This is useful for debugging in development + if settings.DEBUG: + return JsonResponse({"detail": str(exc)}, status=HTTPStatus.INTERNAL_SERVER_ERROR) + return exceptions.server_error(context["request"]) + + return response diff --git a/promgen/migrations/0026_siteconfiguration.py b/promgen/migrations/0026_siteconfiguration.py new file mode 100644 index 000000000..6924a1d96 --- /dev/null +++ b/promgen/migrations/0026_siteconfiguration.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.11 on 2025-04-10 01:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('promgen', '0025_farm_owner'), + ] + + operations = [ + migrations.CreateModel( + name='SiteConfiguration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('key', models.CharField(max_length=128, unique=True)), + ('value', models.JSONField(default=dict)), + ], + ), + ] diff --git a/promgen/models.py b/promgen/models.py index f4f4e92a3..df06c2bbc 100644 --- a/promgen/models.py +++ b/promgen/models.py @@ -620,3 +620,11 @@ class Meta: ordering = ["shard", "host"] unique_together = (("host", "port"),) verbose_name_plural = "prometheis" + + +class SiteConfiguration(models.Model): + key = models.CharField(max_length=128, unique=True) + value = models.JSONField(default=dict) + + def __str__(self): + return f"{self.key}={self.value}" diff --git a/promgen/rest_v2.py b/promgen/rest_v2.py new file mode 100644 index 000000000..7622d76fa --- /dev/null +++ b/promgen/rest_v2.py @@ -0,0 +1,655 @@ +# Copyright (c) 2017 LINE Corporation +# These sources are released under the terms of the MIT license: see LICENSE +from http import HTTPStatus + +from django.contrib.contenttypes.models import ContentType +from django.contrib.auth.models import User +from drf_spectacular.utils import ( + extend_schema, + extend_schema_view, + OpenApiParameter, +) +from rest_framework import mixins, pagination, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from promgen import filters, models, serializers, signals + + +class RuleMixin: + @extend_schema( + summary="List Rules", + description="Retrieve all rules associated with the specified object.", + responses=serializers.RuleSerializer(many=True), + ) + @action(detail=True, methods=["get"], pagination_class=None, filterset_class=None) + def rules(self, request, id): + return Response( + serializers.RuleSerializer(self.get_object().rule_set.all(), many=True).data + ) + + @extend_schema( + summary="Register Rule", + description="Register a new rule for the specified object.", + request=serializers.RuleSerializer, + responses={201: serializers.RuleSerializer(many=True)}, + ) + @rules.mapping.post + def register_rule(self, request, id): + serializer = serializers.RuleSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + object = self.get_object() + + attributes = { + "content_type_id": ContentType.objects.get_for_model(object).id, + "object_id": object.id, + } + + for field in serializer.fields: + value = serializer.validated_data.get(field) + if value is not None: + attributes[field] = value + + rule, _ = models.Rule.objects.get_or_create(**attributes) + return Response( + serializers.RuleSerializer(object.rule_set, many=True).data, status=HTTPStatus.CREATED + ) + + +class NotifierMixin: + @extend_schema( + summary="List Notifiers", + description="Retrieve all notifiers associated with the specified object.", + responses=serializers.SenderSerializer(many=True), + ) + @action(detail=True, methods=["get"], pagination_class=None, filterset_class=None) + def notifiers(self, request, id): + return Response( + serializers.SenderSerializer(self.get_object().notifiers.all(), many=True).data + ) + + @extend_schema( + summary="Register Notifier", + description="Register a new notifier for the specified object.", + request=serializers.RegisterNotifierSerializer, + responses={201: serializers.NotifierSerializer(many=True)}, + ) + @notifiers.mapping.post + def register_notifier(self, request, id): + serializer = serializers.RegisterNotifierSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + object = self.get_object() + + attributes = { + "content_type_id": ContentType.objects.get_for_model(object).id, + "object_id": object.id, + } + + for field in serializer.fields: + value = serializer.validated_data.get(field) + if value is not None and field != "filters": + attributes[field] = value + + notifier, _ = models.Sender.objects.get_or_create(**attributes) + for filter_data in serializer.validated_data.get("filters", []): + models.Filter.objects.get_or_create(sender=notifier, **filter_data) + return Response( + serializers.NotifierSerializer(object.notifiers, many=True).data, + status=HTTPStatus.CREATED, + ) + + +class PromgenPagination(pagination.PageNumberPagination): + page_query_param = "page_number" + page_size_query_param = "page_size" + page_size = 100 # Default page size + max_page_size = 1000 + + +@extend_schema_view( + list=extend_schema(summary="List Users", description="Retrieve a list of all users."), +) +@extend_schema(tags=["User"]) +class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = User.objects.all().order_by("id") + filterset_class = filters.UserFilter + serializer_class = serializers.UserSerializer + lookup_value_regex = "[^/]+" + pagination_class = PromgenPagination + + @extend_schema( + summary="Get Current User", + description="Retrieve the current authenticated user's information.", + responses=serializers.CurrentUserSerializer, + ) + @action(detail=False, methods=["get"], url_path="me", permission_classes=[IsAuthenticated]) + def get_current_user(self, request): + return Response(serializers.CurrentUserSerializer(request.user).data) + + +@extend_schema_view( + list=extend_schema(summary="List Audit Logs", description="Retrieve a list of all audit logs."), +) +@extend_schema(tags=["Log"]) +class AuditViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = models.Audit.objects.all().order_by("-created") + filterset_class = filters.AuditFilter + serializer_class = serializers.AuditSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + +@extend_schema_view( + list=extend_schema(summary="List Notifiers", description="Retrieve a list of all notifiers."), + update=extend_schema(summary="Update Notifier", description="Update an existing notifier."), + partial_update=extend_schema( + summary="Partially Update Notifier", description="Partially update an existing notifier." + ), + destroy=extend_schema(summary="Delete Notifier", description="Delete an existing notifier."), +) +@extend_schema(tags=["Notifier"]) +class NotifierViewSet( + mixins.ListModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + queryset = models.Sender.objects.all().order_by("value") + filterset_class = filters.NotifierFilter + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + def get_serializer_class(self): + if self.action == "list": + return serializers.NotifierSerializer + if self.action == "update": + return serializers.UpdateNotifierSerializer + if self.action == "partial_update": + return serializers.UpdateNotifierSerializer + return serializers.NotifierSerializer + + @extend_schema( + summary="Add Filter", + description="Add a filter to the specified notifier.", + request=serializers.FilterSerializer, + responses=serializers.NotifierSerializer, + ) + @action(detail=True, methods=["post"], url_path="filters") + def add_filter(self, request, id): + notifier = self.get_object() + serializer = serializers.FilterSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + models.Filter.objects.create( + sender=notifier, + name=serializer.validated_data["name"], + value=serializer.validated_data["value"], + ) + return Response(serializers.NotifierSerializer(notifier).data, status=HTTPStatus.CREATED) + + @extend_schema( + summary="Delete Filter", + description="Delete a filter from the specified notifier.", + ) + @action(detail=True, methods=["delete"], url_path="filters/(?P\d+)") + def delete_filter(self, request, id, filter_id): + notifier = self.get_object() + if notifier: + models.Filter.objects.filter(pk=filter_id).delete() + return Response(status=HTTPStatus.NO_CONTENT) + + +@extend_schema_view( + list=extend_schema(summary="List Rules", description="Retrieve a list of all rules."), + retrieve=extend_schema( + summary="Retrieve Rule", description="Retrieve detailed information about a specific rule." + ), + update=extend_schema(summary="Update Rule", description="Update an existing rule."), + partial_update=extend_schema( + summary="Partially Update Rule", description="Partially update an existing rule." + ), + destroy=extend_schema(summary="Delete Rule", description="Delete an existing rule."), +) +@extend_schema(tags=["Rule"]) +class RuleViewSet( + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + queryset = models.Rule.objects.all() + filterset_class = filters.RuleFilterV2 + serializer_class = serializers.RuleSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + +@extend_schema_view( + list=extend_schema(summary="List Farms", description="Retrieve a list of all farms."), + retrieve=extend_schema( + summary="Retrieve Farm", description="Retrieve detailed information about a specific farm." + ), + create=extend_schema(summary="Create Farm", description="Create a new farm."), + update=extend_schema(summary="Update Farm", description="Update an existing farm."), + partial_update=extend_schema( + summary="Partially Update Farm", description="Partially update an existing farm." + ), + destroy=extend_schema(summary="Delete Farm", description="Delete an existing farm."), +) +@extend_schema(tags=["Farm"]) +class FarmViewSet(viewsets.ModelViewSet): + queryset = models.Farm.objects.all() + filterset_class = filters.FarmFilter + serializer_class = serializers.FarmRetrieveSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + def get_serializer_class(self): + if self.action == "list": + return serializers.FarmRetrieveSerializer + if self.action == "retrieve": + return serializers.FarmRetrieveSerializer + if self.action == "create": + return serializers.FarmRetrieveSerializer + if self.action == "update": + return serializers.FarmUpdateSerializer + if self.action == "partial_update": + return serializers.FarmUpdateSerializer + return serializers.FarmRetrieveSerializer + + @extend_schema( + summary="List Hosts in Farm", + description="Retrieve all hosts associated with the specified farm.", + parameters=[ + OpenApiParameter(name="page_number", required=False, type=int), + OpenApiParameter(name="page_size", required=False, type=int), + ], + responses=serializers.HostRetrieveSerializer(many=True), + ) + @action(detail=True, methods=["get"], filterset_class=None) + def hosts(self, request, id): + farm = self.get_object() + hosts = farm.host_set.all() + page = self.paginate_queryset(hosts) + return self.get_paginated_response(serializers.HostRetrieveSerializer(page, many=True).data) + + @extend_schema( + summary="List Projects in Farm", + description="Retrieve all projects associated with the specified farm.", + parameters=[ + OpenApiParameter(name="page_number", required=False, type=int), + OpenApiParameter(name="page_size", required=False, type=int), + ], + responses=serializers.ProjectV2Serializer(many=True), + ) + @action(detail=True, methods=["get"], filterset_class=None) + def projects(self, request, id): + farm = self.get_object() + projects = farm.project_set.all() + page = self.paginate_queryset(projects) + return self.get_paginated_response(serializers.ProjectV2Serializer(page, many=True).data) + + @extend_schema( + summary="Register Hosts", + description="Register new hosts for the specified farm.", + request=serializers.HostListSerializer, + responses={201: None}, + ) + @hosts.mapping.post + def register_host(self, request, id): + farm = self.get_object() + hostnames = request.data.get("hosts", []) + for hostname in hostnames: + models.Host.objects.get_or_create(name=hostname, farm_id=farm.id) + return Response(status=HTTPStatus.CREATED) + + @extend_schema( + summary="Delete Hosts", + description="Delete hosts from the specified farm.", + ) + @action( + detail=True, methods=["delete"], url_path="hosts/(?P\d+)", pagination_class=None + ) + def delete_host(self, request, id, host_id): + farm = self.get_object() + models.Host.objects.filter(pk=host_id, farm=farm).delete() + return Response(status=HTTPStatus.NO_CONTENT) + + +@extend_schema_view( + list=extend_schema(summary="List Exporters", description="Retrieve a list of all exporters."), + retrieve=extend_schema( + summary="Retrieve Exporter", + description="Retrieve detailed information about a specific exporter.", + ), +) +@extend_schema(tags=["Exporter"]) +class ExporterViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = models.Exporter.objects.all() + filterset_class = filters.ExporterFilter + serializer_class = serializers.ExporterSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + +@extend_schema_view( + list=extend_schema(summary="List URLs", description="Retrieve a list of all URLs."), + retrieve=extend_schema( + summary="Retrieve URL", description="Retrieve detailed information about a specific URL." + ), +) +@extend_schema(tags=["URL"]) +class URLViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = models.URL.objects.all() + filterset_class = filters.URLFilter + serializer_class = serializers.URLSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + +@extend_schema_view( + list=extend_schema(summary="List Projects", description="Retrieve a list of all projects."), + retrieve=extend_schema( + summary="Retrieve Project", + description="Retrieve detailed information about a specific project.", + ), + create=extend_schema(summary="Create Project", description="Create a new project."), + update=extend_schema(summary="Update Project", description="Update an existing project."), + partial_update=extend_schema( + summary="Partially Update Project", description="Partially update an existing project." + ), + destroy=extend_schema(summary="Delete Project", description="Delete an existing project."), +) +@extend_schema(tags=["Project"]) +class ProjectViewSet(NotifierMixin, RuleMixin, viewsets.ModelViewSet): + queryset = models.Project.objects.prefetch_related("service", "shard", "farm") + filterset_class = filters.ProjectFilterV2 + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + def get_serializer_class(self): + if self.action == "list": + return serializers.ProjectV2Serializer + if self.action == "retrieve": + return serializers.ProjectV2Serializer + if self.action == "create": + return serializers.ProjectV2Serializer + if self.action == "update": + return serializers.ProjectUpdateSerializer + if self.action == "partial_update": + return serializers.ProjectUpdateSerializer + return serializers.ProjectV2Serializer + + @extend_schema( + summary="List Exporters", + description="Retrieve all exporters associated with the specified project.", + responses=serializers.ExporterSerializer(many=True), + ) + @action(detail=True, methods=["get"], pagination_class=None, filterset_class=None) + def exporters(self, request, id): + project = self.get_object() + return Response(serializers.ExporterSerializer(project.exporter_set.all(), many=True).data) + + @extend_schema( + summary="List URLs", + description="Retrieve all URLs associated with the specified project.", + responses=serializers.URLSerializer(many=True), + ) + @action(detail=True, methods=["get"], pagination_class=None, filterset_class=None) + def urls(self, request, id): + project = self.get_object() + return Response(serializers.URLSerializer(project.url_set.all(), many=True).data) + + @extend_schema( + summary="Link Farm", + description="Link a farm to the specified project.", + request=serializers.LinkFarmSerializer, + responses=serializers.ProjectV2Serializer, + ) + @action(detail=True, methods=["patch"], url_path="farm-link") + def link_farm(self, request, id): + serializer = serializers.LinkFarmSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + project = self.get_object() + farm, created = models.Farm.objects.get_or_create( + name=serializer.validated_data["farm"], + source=serializer.validated_data["source"], + ) + if created: + farm.refresh() + project.farm = farm + project.save() + return Response(serializers.ProjectV2Serializer(project).data) + + @extend_schema( + summary="Unlink Farm", + description="Unlink the farm from the specified project.", + responses=serializers.ProjectV2Serializer, + ) + @action(detail=True, methods=["patch"], url_path="farm-unlink") + def unlink_farm(self, request, id): + project = self.get_object() + if project.farm is None: + return Response(serializers.ProjectV2Serializer(project).data) + + old_farm, project.farm = project.farm, None + project.save() + signals.trigger_write_config.send(request) + + if old_farm.project_set.count() == 0 and old_farm.editable is False: + old_farm.delete() + return Response(serializers.ProjectV2Serializer(project).data) + + @extend_schema( + summary="Register URL", + description="Register a new URL for the specified project.", + request=serializers.RegisterURLProjectSerializer, + responses={201: serializers.URLSerializer(many=True)}, + ) + @urls.mapping.post + def register_url(self, request, id): + serializer = serializers.RegisterURLProjectSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + project = self.get_object() + + models.URL.objects.get_or_create( + project=project, + url=serializer.validated_data["url"], + probe=serializer.validated_data["probe"], + ) + return Response( + serializers.URLSerializer(project.url_set, many=True).data, status=HTTPStatus.CREATED + ) + + @extend_schema( + summary="Delete URL", + description="Delete a URL from the specified project.", + ) + @action( + detail=True, + methods=["delete"], + url_path="urls/(?P\d+)", + pagination_class=None, + filterset_class=None, + ) + def delete_url(self, request, id, url_id): + project = self.get_object() + models.URL.objects.filter(project=project, pk=url_id).delete() + return Response(status=HTTPStatus.NO_CONTENT) + + @extend_schema( + summary="Register Exporter", + description="Register a new exporter for the specified project.", + request=serializers.RegisterExporterProjectSerializer, + responses={201: serializers.ExporterSerializer(many=True)}, + ) + @exporters.mapping.post + def register_exporter(self, request, id): + serializer = serializers.RegisterExporterProjectSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + project = self.get_object() + + attributes = { + "project_id": project.id, + } + + for field in serializer.fields: + value = serializer.validated_data.get(field) + if value is not None: + attributes[field] = value + + models.Exporter.objects.get_or_create(**attributes) + return Response( + serializers.ExporterSerializer(project.exporter_set, many=True).data, + status=HTTPStatus.CREATED, + ) + + @extend_schema( + summary="Update Exporter", + description="Update an existing exporter for the specified project.", + request=serializers.UpdateExporterProjectSerializer, + responses=serializers.ExporterSerializer(many=True), + ) + @action( + detail=True, + methods=["patch"], + url_path="exporters/(?P\d+)", + pagination_class=None, + filterset_class=None, + ) + def update_exporter(self, request, id, exporter_id): + serializer = serializers.UpdateExporterProjectSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + project = self.get_object() + exporter = models.Exporter.objects.filter(project=project, pk=exporter_id).first() + exporter.enabled = serializer.validated_data.get("enabled") + exporter.save() + return Response(serializers.ExporterSerializer(project.exporter_set, many=True).data) + + @extend_schema( + summary="Delete Exporter", + description="Delete an exporter from the specified project.", + ) + @update_exporter.mapping.delete + def delete_exporter(self, request, id, exporter_id): + project = self.get_object() + models.Exporter.objects.filter(project=project, pk=exporter_id).delete() + return Response(status=HTTPStatus.NO_CONTENT) + + +@extend_schema_view( + list=extend_schema( + summary="List Services", + description="Retrieve a list of all services.", + ), + retrieve=extend_schema( + summary="Retrieve Service", + description="Retrieve detailed information about a specific service.", + ), + create=extend_schema(summary="Create Service", description="Create a new service."), + update=extend_schema(summary="Update Service", description="Update an existing service."), + partial_update=extend_schema( + summary="Partially Update Service", description="Partially update an existing service." + ), + destroy=extend_schema(summary="Delete Service", description="Delete an existing service."), +) +@extend_schema(tags=["Service"]) +class ServiceViewSet(NotifierMixin, RuleMixin, viewsets.ModelViewSet): + model = "Service" + queryset = models.Service.objects.all() + filterset_class = filters.ServiceFilterV2 + serializer_class = serializers.ServiceSerializer + lookup_value_regex = "[^/]+" + lookup_field = "id" + pagination_class = PromgenPagination + + def get_serializer_class(self): + if self.action == "list": + return serializers.ServiceV2Serializer + if self.action == "retrieve": + return serializers.ServiceV2Serializer + if self.action == "create": + return serializers.ServiceV2Serializer + if self.action == "update": + return serializers.ServiceUpdateSerializer + if self.action == "partial_update": + return serializers.ServiceUpdateSerializer + return serializers.ServiceV2Serializer + + @extend_schema( + summary="List Projects", + description="Retrieve all projects associated with the specified service.", + responses=serializers.ProjectV2Serializer(many=True), + ) + @action(detail=True, methods=["get"], pagination_class=None, filterset_class=None) + def projects(self, request, id): + service = self.get_object() + return Response( + serializers.ProjectV2Serializer(service.project_set.all(), many=True).data + ) + + @extend_schema( + summary="Register Project", + description="Register a new project for the specified service.", + request=serializers.ProjectUpdateSerializer, + responses={201: serializers.ProjectV2Serializer}, + ) + @projects.mapping.post + def register_project(self, request, id): + serializer = serializers.ProjectUpdateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + service = self.get_object() + + attributes = {"service": service} + + for field in serializer.fields: + value = serializer.validated_data.get(field) + if value is not None: + attributes[field] = value + + project, _ = models.Project.objects.get_or_create(**attributes) + return Response( + serializers.ProjectV2Serializer(project).data, status=HTTPStatus.CREATED + ) + + +@extend_schema_view( + list=extend_schema(summary="List Shards", description="Retrieve a list of all shards."), + retrieve=extend_schema( + summary="Retrieve Shard", + description="Retrieve detailed information about a specific shard.", + ), +) +@extend_schema(tags=["Shard"]) +class ShardViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = models.Shard.objects.all() + filterset_class = filters.ShardFilter + serializer_class = serializers.ShardRetrieveSerializer + lookup_field = "id" + pagination_class = PromgenPagination + + @extend_schema( + summary="List Projects in Shard", + description="Retrieve all projects associated with the specified shard.", + parameters=[ + OpenApiParameter(name="page_number", required=False, type=int), + OpenApiParameter(name="page_size", required=False, type=int), + ], + responses=serializers.ProjectV2Serializer(many=True), + ) + @action(detail=True, methods=["get"], filterset_class=None) + def projects(self, request, id): + shard = self.get_object() + projects = shard.project_set.all() + page = self.paginate_queryset(projects) + return self.get_paginated_response( + serializers.ProjectV2Serializer(page, many=True).data + ) diff --git a/promgen/schemas.py b/promgen/schemas.py new file mode 100644 index 000000000..58a782f52 --- /dev/null +++ b/promgen/schemas.py @@ -0,0 +1,40 @@ +from drf_spectacular.openapi import AutoSchema +from drf_spectacular.plumbing import get_relative_url, set_query_parameters +from drf_spectacular.settings import spectacular_settings +from drf_spectacular.utils import extend_schema +from drf_spectacular.views import AUTHENTICATION_CLASSES +from rest_framework.renderers import TemplateHTMLRenderer +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.views import APIView + + +class CustomSchema(AutoSchema): + def is_excluded(self): + return not self.path.startswith("/rest/v2/") or self.path.startswith("/rest/v2/schema/") + + +class SpectacularRapiDocView(APIView): + renderer_classes = [TemplateHTMLRenderer] + permission_classes = spectacular_settings.SERVE_PERMISSIONS + authentication_classes = AUTHENTICATION_CLASSES + url_name = "schema" + url = None + template_name = "drf_spectacular/rapidoc.html" + title = spectacular_settings.TITLE + + @extend_schema(exclude=True) + def get(self, request, *args, **kwargs): + return Response( + data={ + "title": self.title, + "schema_url": self._get_schema_url(request), + }, + template_name=self.template_name, + ) + + def _get_schema_url(self, request): + schema_url = self.url or get_relative_url(reverse(self.url_name, request=request)) + return set_query_parameters( + url=schema_url, lang=request.GET.get("lang"), version=request.GET.get("version") + ) diff --git a/promgen/serializers.py b/promgen/serializers.py index bddce8805..12e530c15 100644 --- a/promgen/serializers.py +++ b/promgen/serializers.py @@ -1,10 +1,13 @@ import collections +from django.contrib.auth.models import User from django.db.models import prefetch_related_objects +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers import promgen.templatetags.promgen as macro -from promgen import models, shortcuts +from promgen import models, shortcuts, discovery from promgen.shortcuts import resolve_domain @@ -50,7 +53,7 @@ class Meta: class SenderSerializer(serializers.ModelSerializer): owner = serializers.ReadOnlyField(source="owner.username") - label = serializers.ReadOnlyField(source="show_value") + label = serializers.CharField(source="show_value", read_only=True) class Meta: model = models.Sender @@ -119,3 +122,295 @@ class HostSerializer(serializers.ModelSerializer): class Meta: model = models.Host exclude = ("id", "farm") + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ("id", "username", "email", "first_name", "last_name") + + +class CurrentUserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ( + "id", + "username", + "email", + "first_name", + "last_name", + "is_staff", + "is_superuser", + "date_joined", + "last_login", + ) + + +class AuditSerializer(serializers.ModelSerializer): + user = serializers.ReadOnlyField(source="user.username") + log = serializers.ReadOnlyField(source="body") + content_type = serializers.ReadOnlyField(source="content_type.model") + new = serializers.ReadOnlyField(source="data") + + class Meta: + model = models.Audit + fields = ("user", "content_type", "object_id", "log", "created", "new", "old") + + +class FilterSerializer(serializers.ModelSerializer): + class Meta: + model = models.Filter + fields = ["id", "name", "value"] + + +class NotifierSerializer(serializers.ModelSerializer): + owner = serializers.ReadOnlyField(source="owner.username") + content_name = serializers.SerializerMethodField() + content_type = serializers.ReadOnlyField(source="content_type.model") + filters = FilterSerializer(many=True, read_only=True, source="filter_set") + + class Meta: + model = models.Sender + exclude = ("object_id",) + + def get_content_name(self, obj) -> str: + if hasattr(obj, "content_object"): + if hasattr(obj.content_object, "name"): + return obj.content_object.name + if hasattr(obj.content_object, "username"): + return obj.content_object.username + return None + + +class UpdateNotifierSerializer(serializers.ModelSerializer): + class Meta: + model = models.Sender + fields = ["enabled"] + + +@extend_schema_field(OpenApiTypes.STR) +class RuleField(serializers.Field): + def to_internal_value(self, data): + try: + rule = models.Rule.objects.get(pk=data) + except models.Rule.DoesNotExist: + raise serializers.ValidationError("Owner does not exist.") + return rule + + def to_representation(self, value): + return value.name + + +class RuleSerializer(serializers.ModelSerializer): + content_name = serializers.SerializerMethodField() + content_type = serializers.ReadOnlyField(source="content_type.model") + labels = serializers.JSONField(required=False) + annotations = serializers.JSONField(required=False) + parent = RuleField(required=False) + + class Meta: + model = models.Rule + exclude = ("object_id",) + + def get_content_name(self, obj) -> str: + if hasattr(obj, "content_object"): + return obj.content_object.name + return None + + +class HostRetrieveSerializer(serializers.ModelSerializer): + class Meta: + model = models.Host + fields = "__all__" + + +@extend_schema_field(OpenApiTypes.STR) +class OwnerField(serializers.Field): + def to_internal_value(self, data): + if not data: + return serializers.CurrentUserDefault() + try: + owner = User.objects.get(username=data) + except User.DoesNotExist: + raise serializers.ValidationError("Owner does not exist.") + return owner + + def to_representation(self, value): + return value.username + + +class FarmRetrieveSerializer(serializers.ModelSerializer): + owner = OwnerField(required=False, default=serializers.CurrentUserDefault()) + + class Meta: + model = models.Farm + fields = "__all__" + + +class FarmUpdateSerializer(serializers.ModelSerializer): + owner = OwnerField(required=False) + + class Meta: + model = models.Farm + fields = "__all__" + + +class HostListSerializer(serializers.Serializer): + hosts = serializers.ListField( + child=serializers.CharField(), help_text="List of hostnames to add." + ) + + +class ExporterSerializer(serializers.ModelSerializer): + project = serializers.ReadOnlyField(source="project.name") + + class Meta: + model = models.Exporter + fields = "__all__" + + +class URLSerializer(serializers.ModelSerializer): + project = serializers.ReadOnlyField(source="project.name") + probe = serializers.ReadOnlyField(source="probe.module") + + class Meta: + model = models.URL + fields = "__all__" + + +@extend_schema_field(OpenApiTypes.STR) +class ServiceField(serializers.Field): + def to_internal_value(self, data): + try: + service = models.Service.objects.get(name=data) + except models.Service.DoesNotExist: + raise serializers.ValidationError("Service does not exist.") + return service + + def to_representation(self, value): + return value.name + + +@extend_schema_field(OpenApiTypes.STR) +class ShardField(serializers.Field): + def to_internal_value(self, data): + try: + shard = models.Shard.objects.get(name=data) + except models.Shard.DoesNotExist: + raise serializers.ValidationError("Shard does not exist.") + return shard + + def to_representation(self, value): + return value.name + + +@extend_schema_field(OpenApiTypes.STR) +class FarmField(serializers.Field): + def to_internal_value(self, data): + try: + farm = models.Farm.objects.get(name=data, source=discovery.FARM_DEFAULT) + except models.Farm.DoesNotExist: + raise serializers.ValidationError("Farm does not exist.") + return farm + + def to_representation(self, value): + return value.name + + +class ProjectV2Serializer(serializers.ModelSerializer): + owner = OwnerField(required=False, default=serializers.CurrentUserDefault()) + service = ServiceField() + shard = ShardField() + farm = FarmField(required=False) + + class Meta: + model = models.Project + fields = "__all__" + + +class ProjectUpdateSerializer(serializers.ModelSerializer): + owner = OwnerField(required=False) + service = ServiceField(required=False, read_only=True) + shard = ShardField(required=False) + farm = serializers.ReadOnlyField(source="farm.name") + name = serializers.CharField(required=False) + + class Meta: + model = models.Project + fields = "__all__" + + +class LinkFarmSerializer(serializers.Serializer): + farm = serializers.CharField() + source = serializers.ChoiceField(choices=[name for name, _ in models.Farm.driver_set()]) + + +@extend_schema_field(OpenApiTypes.STR) +class ProbeField(serializers.Field): + def to_internal_value(self, data): + try: + probe = models.Probe.objects.get(module=data) + except models.Probe.DoesNotExist: + raise serializers.ValidationError("Probe does not exist.") + return probe + + def to_representation(self, value): + return value.module + + +class RegisterURLProjectSerializer(serializers.Serializer): + url = serializers.CharField() + probe = ProbeField() + + +class RegisterExporterProjectSerializer(serializers.Serializer): + job = serializers.CharField() + port = serializers.IntegerField() + path = serializers.CharField() + scheme = serializers.ChoiceField(choices=["http", "https"]) + enabled = serializers.BooleanField(required=False) + + +class UpdateExporterProjectSerializer(serializers.Serializer): + enabled = serializers.BooleanField() + + +class DeleteExporterProjectSerializer(serializers.Serializer): + job = serializers.CharField() + port = serializers.IntegerField() + path = serializers.CharField() + scheme = serializers.CharField() + + +class RegisterNotifierSerializer(serializers.Serializer): + sender = serializers.ChoiceField(choices=[name for name, _ in models.Sender.driver_set()]) + value = serializers.CharField() + alias = serializers.CharField(required=False) + enabled = serializers.BooleanField(required=False, default=True) + filters = FilterSerializer(many=True, required=False) + + +class ServiceV2Serializer(serializers.ModelSerializer): + owner = OwnerField(required=False, default=serializers.CurrentUserDefault()) + id = serializers.ReadOnlyField() + + class Meta: + model = models.Service + fields = "__all__" + + +class ServiceUpdateSerializer(serializers.ModelSerializer): + owner = OwnerField(required=False) + name = serializers.CharField(required=False) + description = serializers.CharField(required=False) + id = serializers.ReadOnlyField() + + class Meta: + model = models.Service + fields = "__all__" + + +class ShardRetrieveSerializer(serializers.ModelSerializer): + class Meta: + model = models.Shard + exclude = ("authorization",) diff --git a/promgen/settings.py b/promgen/settings.py index 190385e29..c6c61e173 100644 --- a/promgen/settings.py +++ b/promgen/settings.py @@ -62,6 +62,7 @@ "promgen", # Third Party "django_filters", + "drf_spectacular", "rest_framework.authtoken", "rest_framework", "social_django", @@ -192,10 +193,18 @@ "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", ), - "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly", - ), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoModelPermissions",), "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_SCHEMA_CLASS": "promgen.schemas.CustomSchema", + "EXCEPTION_HANDLER": "promgen.middleware.custom_exception_handler", + "DEFAULT_THROTTLE_CLASSES": [ + "promgen.util.UserRateThrottle", + ], + "DEFAULT_THROTTLE_RATES": { + # Limits the rate of API calls that may be made by a given user. + # The user id will be used as a unique cache key. + "user": "1000/day", + }, } # If CELERY_BROKER_URL is set in our environment, then we configure celery as @@ -222,3 +231,11 @@ globals()[k] = v DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +SPECTACULAR_SETTINGS = { + "TITLE": "Promgen's APIs", + "VERSION": PROMGEN_VERSION, + "DESCRIPTION": "Non-GET APIs require authentication. " + "Please login Promgen and access the Profile page to get a Token.", +} + +ENABLE_API_LOGGING = env.bool("ENABLE_API_LOGGING", default=False) diff --git a/promgen/signals.py b/promgen/signals.py index 2116035fa..7fef44cc2 100644 --- a/promgen/signals.py +++ b/promgen/signals.py @@ -333,3 +333,12 @@ def add_default_project_subscription(instance, created, **kwargs): value=instance.owner.username, defaults={"owner": instance.owner}, ) + + +@receiver(post_save, sender=models.SiteConfiguration) +@skip_raw +def clear_cache(*, sender, instance, **kwargs): + # We need to clear our cache when we change our configuration + # so that we can pick up the new settings + if instance.key == "THROTTLE_RATES": + cache.clear() diff --git a/promgen/static/js/rapidoc.min.js b/promgen/static/js/rapidoc.min.js new file mode 100644 index 000000000..95ba22ce5 --- /dev/null +++ b/promgen/static/js/rapidoc.min.js @@ -0,0 +1,3921 @@ +/** + * Minified by jsDelivr using Terser v5.19.2. + * Original file: /npm/rapidoc@9.3.8/dist/rapidoc-min.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! RapiDoc 9.3.8 | Author - Mrinmoy Majumdar | License information can be found in rapidoc-min.js.LICENSE.txt */ +(()=>{var e,t,r={557:(e,t,r)=>{"use strict";const s=globalThis,n=s.ShadowRoot&&(void 0===s.ShadyCSS||s.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,i=Symbol(),o=new WeakMap;class a{constructor(e,t,r){if(this._$cssResult$=!0,r!==i)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(n&&void 0===e){const r=void 0!==t&&1===t.length;r&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),r&&o.set(t,e))}return e}toString(){return this.cssText}}const l=e=>new a("string"==typeof e?e:e+"",void 0,i),c=(e,...t)=>{const r=1===e.length?e[0]:t.reduce(((t,r,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+e[s+1]),e[0]);return new a(r,e,i)},p=n?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const r of e.cssRules)t+=r.cssText;return l(t)})(e):e,{is:u,defineProperty:d,getOwnPropertyDescriptor:h,getOwnPropertyNames:m,getOwnPropertySymbols:f,getPrototypeOf:g}=Object,y=globalThis,v=y.trustedTypes,b=v?v.emptyScript:"",x=y.reactiveElementPolyfillSupport,w=(e,t)=>e,$={toAttribute(e,t){switch(t){case Boolean:e=e?b:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let r=e;switch(t){case Boolean:r=null!==e;break;case Number:r=null===e?null:Number(e);break;case Object:case Array:try{r=JSON.parse(e)}catch(e){r=null}}return r}},S=(e,t)=>!u(e,t),E={attribute:!0,type:String,converter:$,reflect:!1,hasChanged:S};Symbol.metadata??=Symbol("metadata"),y.litPropertyMetadata??=new WeakMap;class k extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=E){if(t.state&&(t.attribute=!1),this._$Ei(),this.elementProperties.set(e,t),!t.noAccessor){const r=Symbol(),s=this.getPropertyDescriptor(e,r,t);void 0!==s&&d(this.prototype,e,s)}}static getPropertyDescriptor(e,t,r){const{get:s,set:n}=h(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get(){return s?.call(this)},set(t){const i=s?.call(this);n.call(this,t),this.requestUpdate(e,i,r)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??E}static _$Ei(){if(this.hasOwnProperty(w("elementProperties")))return;const e=g(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(w("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(w("properties"))){const e=this.properties,t=[...m(e),...f(e)];for(const r of t)this.createProperty(r,e[r])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,r]of t)this.elementProperties.set(e,r)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const r=this._$Eu(e,t);void 0!==r&&this._$Eh.set(r,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const r=new Set(e.flat(1/0).reverse());for(const e of r)t.unshift(p(e))}else void 0!==e&&t.push(p(e));return t}static _$Eu(e,t){const r=t.attribute;return!1===r?void 0:"string"==typeof r?r:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise((e=>this.enableUpdating=e)),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach((e=>e(this)))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const r of t.keys())this.hasOwnProperty(r)&&(e.set(r,this[r]),delete this[r]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{if(n)e.adoptedStyleSheets=t.map((e=>e instanceof CSSStyleSheet?e:e.styleSheet));else for(const r of t){const t=document.createElement("style"),n=s.litNonce;void 0!==n&&t.setAttribute("nonce",n),t.textContent=r.cssText,e.appendChild(t)}})(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach((e=>e.hostConnected?.()))}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach((e=>e.hostDisconnected?.()))}attributeChangedCallback(e,t,r){this._$AK(e,r)}_$EC(e,t){const r=this.constructor.elementProperties.get(e),s=this.constructor._$Eu(e,r);if(void 0!==s&&!0===r.reflect){const n=(void 0!==r.converter?.toAttribute?r.converter:$).toAttribute(t,r.type);this._$Em=e,null==n?this.removeAttribute(s):this.setAttribute(s,n),this._$Em=null}}_$AK(e,t){const r=this.constructor,s=r._$Eh.get(e);if(void 0!==s&&this._$Em!==s){const e=r.getPropertyOptions(s),n="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:$;this._$Em=s,this[s]=n.fromAttribute(t,e.type),this._$Em=null}}requestUpdate(e,t,r){if(void 0!==e){if(r??=this.constructor.getPropertyOptions(e),!(r.hasChanged??S)(this[e],t))return;this.P(e,t,r)}!1===this.isUpdatePending&&(this._$ES=this._$ET())}P(e,t,r){this._$AL.has(e)||this._$AL.set(e,t),!0===r.reflect&&this._$Em!==e&&(this._$Ej??=new Set).add(e)}async _$ET(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,r]of e)!0!==r.wrapped||this._$AL.has(t)||void 0===this[t]||this.P(t,this[t],r)}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach((e=>e.hostUpdate?.())),this.update(t)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach((e=>e.hostUpdated?.())),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Ej&&=this._$Ej.forEach((e=>this._$EC(e,this[e]))),this._$EU()}updated(e){}firstUpdated(e){}}k.elementStyles=[],k.shadowRootOptions={mode:"open"},k[w("elementProperties")]=new Map,k[w("finalized")]=new Map,x?.({ReactiveElement:k}),(y.reactiveElementVersions??=[]).push("2.0.4");const A=globalThis,O=A.trustedTypes,j=O?O.createPolicy("lit-html",{createHTML:e=>e}):void 0,T="$lit$",P=`lit$${Math.random().toFixed(9).slice(2)}$`,C="?"+P,I=`<${C}>`,_=document,R=()=>_.createComment(""),F=e=>null===e||"object"!=typeof e&&"function"!=typeof e,M=Array.isArray,L=e=>M(e)||"function"==typeof e?.[Symbol.iterator],D="[ \t\n\f\r]",B=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,q=/-->/g,N=/>/g,U=RegExp(`>|${D}(?:([^\\s"'>=/]+)(${D}*=${D}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),z=/'/g,H=/"/g,V=/^(?:script|style|textarea|title)$/i,W=e=>(t,...r)=>({_$litType$:e,strings:t,values:r}),G=W(1),J=(W(2),W(3),Symbol.for("lit-noChange")),K=Symbol.for("lit-nothing"),Y=new WeakMap,X=_.createTreeWalker(_,129);function Z(e,t){if(!M(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==j?j.createHTML(t):t}const Q=(e,t)=>{const r=e.length-1,s=[];let n,i=2===t?"":3===t?"":"",o=B;for(let t=0;t"===l[0]?(o=n??B,c=-1):void 0===l[1]?c=-2:(c=o.lastIndex-l[2].length,a=l[1],o=void 0===l[3]?U:'"'===l[3]?H:z):o===H||o===z?o=U:o===q||o===N?o=B:(o=U,n=void 0);const u=o===U&&e[t+1].startsWith("/>")?" ":"";i+=o===B?r+I:c>=0?(s.push(a),r.slice(0,c)+T+r.slice(c)+P+u):r+P+(-2===c?t:u)}return[Z(e,i+(e[r]||"")+(2===t?"":3===t?"":"")),s]};class ee{constructor({strings:e,_$litType$:t},r){let s;this.parts=[];let n=0,i=0;const o=e.length-1,a=this.parts,[l,c]=Q(e,t);if(this.el=ee.createElement(l,r),X.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(s=X.nextNode())&&a.length0){s.textContent=O?O.emptyScript:"";for(let r=0;r2||""!==r[0]||""!==r[1]?(this._$AH=Array(r.length-1).fill(new String),this.strings=r):this._$AH=K}_$AI(e,t=this,r,s){const n=this.strings;let i=!1;if(void 0===n)e=te(this,e,t,0),i=!F(e)||e!==this._$AH&&e!==J,i&&(this._$AH=e);else{const s=e;let o,a;for(e=n[0],o=0;o{const s=r?.renderBefore??t;let n=s._$litPart$;if(void 0===n){const e=r?.renderBefore??null;s._$litPart$=n=new se(t.insertBefore(R(),e),e,void 0,r??{})}return n._$AI(e),n})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this.o?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this.o?.setConnected(!1)}render(){return J}}ue._$litElement$=!0,ue.finalized=!0,globalThis.litElementHydrateSupport?.({LitElement:ue});const de=globalThis.litElementPolyfillSupport;de?.({LitElement:ue}),(globalThis.litElementVersions??=[]).push("4.1.0");let he={async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1};const me=/[&<>"']/,fe=new RegExp(me.source,"g"),ge=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,ye=new RegExp(ge.source,"g"),ve={"&":"&","<":"<",">":">",'"':""","'":"'"},be=e=>ve[e];function xe(e,t){if(t){if(me.test(e))return e.replace(fe,be)}else if(ge.test(e))return e.replace(ye,be);return e}const we=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function $e(e){return e.replace(we,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const Se=/(^|[^\[])\^/g;function Ee(e,t){e="string"==typeof e?e:e.source,t=t||"";const r={replace:(t,s)=>(s=(s=s.source||s).replace(Se,"$1"),e=e.replace(t,s),r),getRegex:()=>new RegExp(e,t)};return r}const ke=/[^\w:]/g,Ae=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function Oe(e,t,r){if(e){let t;try{t=decodeURIComponent($e(r)).replace(ke,"").toLowerCase()}catch(e){return null}if(0===t.indexOf("javascript:")||0===t.indexOf("vbscript:")||0===t.indexOf("data:"))return null}t&&!Ae.test(r)&&(r=function(e,t){je[" "+e]||(Te.test(e)?je[" "+e]=e+"/":je[" "+e]=Re(e,"/",!0));const r=-1===(e=je[" "+e]).indexOf(":");return"//"===t.substring(0,2)?r?t:e.replace(Pe,"$1")+t:"/"===t.charAt(0)?r?t:e.replace(Ce,"$1")+t:e+t}(t,r));try{r=encodeURI(r).replace(/%25/g,"%")}catch(e){return null}return r}const je={},Te=/^[^:]+:\/*[^/]*$/,Pe=/^([^:]+:)[\s\S]*$/,Ce=/^([^:]+:\/*[^/]*)[\s\S]*$/,Ie={exec:function(){}};function _e(e,t){const r=e.replace(/\|/g,((e,t,r)=>{let s=!1,n=t;for(;--n>=0&&"\\"===r[n];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(r[0].trim()||r.shift(),r.length>0&&!r[r.length-1].trim()&&r.pop(),r.length>t)r.splice(t);else for(;r.length1;)1&t&&(r+=e),t>>=1,e+=e;return r+e}function Me(e,t,r,s){const n=t.href,i=t.title?xe(t.title):null,o=e[1].replace(/\\([\[\]])/g,"$1");if("!"!==e[0].charAt(0)){s.state.inLink=!0;const e={type:"link",raw:r,href:n,title:i,text:o,tokens:s.inlineTokens(o)};return s.state.inLink=!1,e}return{type:"image",raw:r,href:n,title:i,text:xe(o)}}class Le{constructor(e){this.options=e||he}space(e){const t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:Re(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],r=function(e,t){const r=e.match(/^(\s+)(?:```)/);if(null===r)return t;const s=r[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[r]=t;return r.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline._escapes,"$1"):t[2],text:r}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=Re(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){const e=t[0].replace(/^ *>[ \t]?/gm,""),r=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=r,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let r,s,n,i,o,a,l,c,p,u,d,h,m=t[1].trim();const f=m.length>1,g={type:"list",raw:"",ordered:f,start:f?+m.slice(0,-1):"",loose:!1,items:[]};m=f?`\\d{1,9}\\${m.slice(-1)}`:`\\${m}`,this.options.pedantic&&(m=f?m:"[*+-]");const y=new RegExp(`^( {0,3}${m})((?:[\t ][^\\n]*)?(?:\\n|$))`);for(;e&&(h=!1,t=y.exec(e))&&!this.rules.block.hr.test(e);){if(r=t[0],e=e.substring(r.length),c=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),p=e.split("\n",1)[0],this.options.pedantic?(i=2,d=c.trimLeft()):(i=t[2].search(/[^ ]/),i=i>4?1:i,d=c.slice(i),i+=t[1].length),a=!1,!c&&/^ *$/.test(p)&&(r+=p+"\n",e=e.substring(p.length+1),h=!0),!h){const t=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),s=new RegExp(`^ {0,${Math.min(3,i-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),n=new RegExp(`^ {0,${Math.min(3,i-1)}}(?:\`\`\`|~~~)`),o=new RegExp(`^ {0,${Math.min(3,i-1)}}#`);for(;e&&(u=e.split("\n",1)[0],p=u,this.options.pedantic&&(p=p.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),!n.test(p))&&!o.test(p)&&!t.test(p)&&!s.test(e);){if(p.search(/[^ ]/)>=i||!p.trim())d+="\n"+p.slice(i);else{if(a)break;if(c.search(/[^ ]/)>=4)break;if(n.test(c))break;if(o.test(c))break;if(s.test(c))break;d+="\n"+p}a||p.trim()||(a=!0),r+=u+"\n",e=e.substring(u.length+1),c=p.slice(i)}}g.loose||(l?g.loose=!0:/\n *\n *$/.test(r)&&(l=!0)),this.options.gfm&&(s=/^\[[ xX]\] /.exec(d),s&&(n="[ ] "!==s[0],d=d.replace(/^\[[ xX]\] +/,""))),g.items.push({type:"list_item",raw:r,task:!!s,checked:n,loose:!1,text:d}),g.raw+=r}g.items[g.items.length-1].raw=r.trimRight(),g.items[g.items.length-1].text=d.trimRight(),g.raw=g.raw.trimRight();const v=g.items.length;for(o=0;o"space"===e.type)),t=e.length>0&&e.some((e=>/\n.*\n/.test(e.raw)));g.loose=t}if(g.loose)for(o=0;o$/,"$1").replace(this.rules.inline._escapes,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline._escapes,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:r,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(t){const e={type:"table",header:_e(t[1]).map((e=>({text:e}))),align:t[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(e.header.length===e.align.length){e.raw=t[0];let r,s,n,i,o=e.align.length;for(r=0;r({text:e})));for(o=e.header.length,s=0;s/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(t[0]):xe(t[0]):t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=Re(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;const r=e.length;let s=0,n=0;for(;n-1){const r=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,r).trim(),t[3]=""}}let r=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(r);e&&(r=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return r=r.trim(),/^$/.test(e)?r.slice(1):r.slice(1,-1)),Me(t,{href:r?r.replace(this.rules.inline._escapes,"$1"):r,title:s?s.replace(this.rules.inline._escapes,"$1"):s},t[0],this.lexer)}}reflink(e,t){let r;if((r=this.rules.inline.reflink.exec(e))||(r=this.rules.inline.nolink.exec(e))){let e=(r[2]||r[1]).replace(/\s+/g," ");if(e=t[e.toLowerCase()],!e){const e=r[0].charAt(0);return{type:"text",raw:e,text:e}}return Me(r,e,r[0],this.lexer)}}emStrong(e,t,r=""){let s=this.rules.inline.emStrong.lDelim.exec(e);if(!s)return;if(s[3]&&r.match(/[\p{L}\p{N}]/u))return;const n=s[1]||s[2]||"";if(!n||n&&(""===r||this.rules.inline.punctuation.exec(r))){const r=s[0].length-1;let n,i,o=r,a=0;const l="*"===s[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(l.lastIndex=0,t=t.slice(-1*e.length+r);null!=(s=l.exec(t));){if(n=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!n)continue;if(i=n.length,s[3]||s[4]){o+=i;continue}if((s[5]||s[6])&&r%3&&!((r+i)%3)){a+=i;continue}if(o-=i,o>0)continue;i=Math.min(i,i+o+a);const t=e.slice(0,r+s.index+(s[0].length-n.length)+i);if(Math.min(r,i)%2){const e=t.slice(1,-1);return{type:"em",raw:t,text:e,tokens:this.lexer.inlineTokens(e)}}const l=t.slice(2,-2);return{type:"strong",raw:t,text:l,tokens:this.lexer.inlineTokens(l)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const r=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return r&&s&&(e=e.substring(1,e.length-1)),e=xe(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e,t){const r=this.rules.inline.autolink.exec(e);if(r){let e,s;return"@"===r[2]?(e=xe(this.options.mangle?t(r[1]):r[1]),s="mailto:"+e):(e=xe(r[1]),s=e),{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}url(e,t){let r;if(r=this.rules.inline.url.exec(e)){let e,s;if("@"===r[2])e=xe(this.options.mangle?t(r[0]):r[0]),s="mailto:"+e;else{let t;do{t=r[0],r[0]=this.rules.inline._backpedal.exec(r[0])[0]}while(t!==r[0]);e=xe(r[0]),s="www."===r[1]?"http://"+r[0]:r[0]}return{type:"link",raw:r[0],text:e,href:s,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e,t){const r=this.rules.inline.text.exec(e);if(r){let e;return e=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(r[0]):xe(r[0]):r[0]:xe(this.options.smartypants?t(r[0]):r[0]),{type:"text",raw:r[0],text:e}}}}const De={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:Ie,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/};De.def=Ee(De.def).replace("label",De._label).replace("title",De._title).getRegex(),De.bullet=/(?:[*+-]|\d{1,9}[.)])/,De.listItemStart=Ee(/^( *)(bull) */).replace("bull",De.bullet).getRegex(),De.list=Ee(De.list).replace(/bull/g,De.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+De.def.source+")").getRegex(),De._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",De._comment=/|$)/,De.html=Ee(De.html,"i").replace("comment",De._comment).replace("tag",De._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),De.paragraph=Ee(De._paragraph).replace("hr",De.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",De._tag).getRegex(),De.blockquote=Ee(De.blockquote).replace("paragraph",De.paragraph).getRegex(),De.normal={...De},De.gfm={...De.normal,table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"},De.gfm.table=Ee(De.gfm.table).replace("hr",De.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",De._tag).getRegex(),De.gfm.paragraph=Ee(De._paragraph).replace("hr",De.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",De.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",De._tag).getRegex(),De.pedantic={...De.normal,html:Ee("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",De._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:Ie,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:Ee(De.normal._paragraph).replace("hr",De.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",De.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()};const Be={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:Ie,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:Ie,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(r="x"+r.toString(16)),s+="&#"+r+";";return s}Be._punctuation="!\"#$%&'()+\\-.,/:;<=>?@\\[\\]`^{|}~",Be.punctuation=Ee(Be.punctuation).replace(/punctuation/g,Be._punctuation).getRegex(),Be.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,Be.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,Be._comment=Ee(De._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),Be.emStrong.lDelim=Ee(Be.emStrong.lDelim).replace(/punct/g,Be._punctuation).getRegex(),Be.emStrong.rDelimAst=Ee(Be.emStrong.rDelimAst,"g").replace(/punct/g,Be._punctuation).getRegex(),Be.emStrong.rDelimUnd=Ee(Be.emStrong.rDelimUnd,"g").replace(/punct/g,Be._punctuation).getRegex(),Be._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,Be._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,Be._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,Be.autolink=Ee(Be.autolink).replace("scheme",Be._scheme).replace("email",Be._email).getRegex(),Be._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,Be.tag=Ee(Be.tag).replace("comment",Be._comment).replace("attribute",Be._attribute).getRegex(),Be._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Be._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,Be._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,Be.link=Ee(Be.link).replace("label",Be._label).replace("href",Be._href).replace("title",Be._title).getRegex(),Be.reflink=Ee(Be.reflink).replace("label",Be._label).replace("ref",De._label).getRegex(),Be.nolink=Ee(Be.nolink).replace("ref",De._label).getRegex(),Be.reflinkSearch=Ee(Be.reflinkSearch,"g").replace("reflink",Be.reflink).replace("nolink",Be.nolink).getRegex(),Be.normal={...Be},Be.pedantic={...Be.normal,strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:Ee(/^!?\[(label)\]\((.*?)\)/).replace("label",Be._label).getRegex(),reflink:Ee(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",Be._label).getRegex()},Be.gfm={...Be.normal,escape:Ee(Be.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\t+" ".repeat(r.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.space(e))e=e.substring(r.raw.length),1===r.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(r);else if(r=this.tokenizer.code(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(r):(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.fences(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.heading(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.hr(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.blockquote(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.list(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.html(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.def(e))e=e.substring(r.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title}):(s.raw+="\n"+r.raw,s.text+="\n"+r.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(r=this.tokenizer.table(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.lheading(e))e=e.substring(r.raw.length),t.push(r);else{if(n=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startBlock.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(this.state.top&&(r=this.tokenizer.paragraph(n)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r),i=n.length!==e.length,e=e.substring(r.raw.length);else if(r=this.tokenizer.text(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+r.raw,s.text+="\n"+r.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let r,s,n,i,o,a,l=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(l));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(l=l.slice(0,i.index)+"["+Fe("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(l));)l=l.slice(0,i.index)+"["+Fe("a",i[0].length-2)+"]"+l.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.escapedEmSt.exec(l));)l=l.slice(0,i.index+i[0].length-2)+"++"+l.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex),this.tokenizer.rules.inline.escapedEmSt.lastIndex--;for(;e;)if(o||(a=""),o=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(r=s.call({lexer:this},e,t))&&(e=e.substring(r.raw.length),t.push(r),!0)))))if(r=this.tokenizer.escape(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.tag(e))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.link(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(r.raw.length),s=t[t.length-1],s&&"text"===r.type&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(r=this.tokenizer.emStrong(e,l,a))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.codespan(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.br(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.del(e))e=e.substring(r.raw.length),t.push(r);else if(r=this.tokenizer.autolink(e,Ne))e=e.substring(r.raw.length),t.push(r);else if(this.state.inLink||!(r=this.tokenizer.url(e,Ne))){if(n=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const r=e.slice(1);let s;this.options.extensions.startInline.forEach((function(e){s=e.call({lexer:this},r),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(n=e.substring(0,t+1))}if(r=this.tokenizer.inlineText(n,qe))e=e.substring(r.raw.length),"_"!==r.raw.slice(-1)&&(a=r.raw.slice(-1)),o=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=r.raw,s.text+=r.text):t.push(r);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(r.raw.length),t.push(r);return t}}class ze{constructor(e){this.options=e||he}code(e,t,r){const s=(t||"").match(/\S*/)[0];if(this.options.highlight){const t=this.options.highlight(e,s);null!=t&&t!==e&&(r=!0,e=t)}return e=e.replace(/\n$/,"")+"\n",s?'
'+(r?e:xe(e,!0))+"
\n":"
"+(r?e:xe(e,!0))+"
\n"}blockquote(e){return`
\n${e}
\n`}html(e){return e}heading(e,t,r,s){return this.options.headerIds?`${e}\n`:`${e}\n`}hr(){return this.options.xhtml?"
\n":"
\n"}list(e,t,r){const s=t?"ol":"ul";return"<"+s+(t&&1!==r?' start="'+r+'"':"")+">\n"+e+"\n"}listitem(e){return`
  • ${e}
  • \n`}checkbox(e){return" "}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const r=t.header?"th":"td";return(t.align?`<${r} align="${t.align}">`:`<${r}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return this.options.xhtml?"
    ":"
    "}del(e){return`${e}`}link(e,t,r){if(null===(e=Oe(this.options.sanitize,this.options.baseUrl,e)))return r;let s='",s}image(e,t,r){if(null===(e=Oe(this.options.sanitize,this.options.baseUrl,e)))return r;let s=`${r}":">",s}text(e){return e}}class He{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,r){return""+r}image(e,t,r){return""+r}br(){return""}}class Ve{constructor(){this.seen={}}serialize(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")}getNextSafeSlug(e,t){let r=e,s=0;if(this.seen.hasOwnProperty(r)){s=this.seen[e];do{s++,r=e+"-"+s}while(this.seen.hasOwnProperty(r))}return t||(this.seen[e]=s,this.seen[r]=0),r}slug(e,t={}){const r=this.serialize(e);return this.getNextSafeSlug(r,t.dryrun)}}class We{constructor(e){this.options=e||he,this.options.renderer=this.options.renderer||new ze,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new He,this.slugger=new Ve}static parse(e,t){return new We(t).parse(e)}static parseInline(e,t){return new We(t).parseInline(e)}parse(e,t=!0){let r,s,n,i,o,a,l,c,p,u,d,h,m,f,g,y,v,b,x,w="";const $=e.length;for(r=0;r<$;r++)if(u=e[r],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[u.type]&&(x=this.options.extensions.renderers[u.type].call({parser:this},u),!1!==x||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(u.type)))w+=x||"";else switch(u.type){case"space":continue;case"hr":w+=this.renderer.hr();continue;case"heading":w+=this.renderer.heading(this.parseInline(u.tokens),u.depth,$e(this.parseInline(u.tokens,this.textRenderer)),this.slugger);continue;case"code":w+=this.renderer.code(u.text,u.lang,u.escaped);continue;case"table":for(c="",l="",i=u.header.length,s=0;s0&&"paragraph"===g.tokens[0].type?(g.tokens[0].text=b+" "+g.tokens[0].text,g.tokens[0].tokens&&g.tokens[0].tokens.length>0&&"text"===g.tokens[0].tokens[0].type&&(g.tokens[0].tokens[0].text=b+" "+g.tokens[0].tokens[0].text)):g.tokens.unshift({type:"text",text:b}):f+=b),f+=this.parse(g.tokens,m),p+=this.renderer.listitem(f,v,y);w+=this.renderer.list(p,d,h);continue;case"html":w+=this.renderer.html(u.text);continue;case"paragraph":w+=this.renderer.paragraph(this.parseInline(u.tokens));continue;case"text":for(p=u.tokens?this.parseInline(u.tokens):u.text;r+1<$&&"text"===e[r+1].type;)u=e[++r],p+="\n"+(u.tokens?this.parseInline(u.tokens):u.text);w+=t?this.renderer.paragraph(p):p;continue;default:{const e='Token with "'+u.type+'" type was not found.';if(this.options.silent)return void console.error(e);throw new Error(e)}}return w}parseInline(e,t){t=t||this.renderer;let r,s,n,i="";const o=e.length;for(r=0;r{"function"==typeof s&&(n=s,s=null);const i={...s},o=function(e,t,r){return s=>{if(s.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+xe(s.message+"",!0)+"
    ";return t?Promise.resolve(e):r?void r(null,e):e}if(t)return Promise.reject(s);if(!r)throw s;r(s)}}((s={...Ke.defaults,...i}).silent,s.async,n);if(null==r)return o(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof r)return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(r)+", string expected"));if(function(e){e&&e.sanitize&&!e.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options")}(s),s.hooks&&(s.hooks.options=s),n){const i=s.highlight;let a;try{s.hooks&&(r=s.hooks.preprocess(r)),a=e(r,s)}catch(e){return o(e)}const l=function(e){let r;if(!e)try{s.walkTokens&&Ke.walkTokens(a,s.walkTokens),r=t(a,s),s.hooks&&(r=s.hooks.postprocess(r))}catch(t){e=t}return s.highlight=i,e?o(e):n(null,r)};if(!i||i.length<3)return l();if(delete s.highlight,!a.length)return l();let c=0;return Ke.walkTokens(a,(function(e){"code"===e.type&&(c++,setTimeout((()=>{i(e.text,e.lang,(function(t,r){if(t)return l(t);null!=r&&r!==e.text&&(e.text=r,e.escaped=!0),c--,0===c&&l()}))}),0))})),void(0===c&&l())}if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(r):r).then((t=>e(t,s))).then((e=>s.walkTokens?Promise.all(Ke.walkTokens(e,s.walkTokens)).then((()=>e)):e)).then((e=>t(e,s))).then((e=>s.hooks?s.hooks.postprocess(e):e)).catch(o);try{s.hooks&&(r=s.hooks.preprocess(r));const n=e(r,s);s.walkTokens&&Ke.walkTokens(n,s.walkTokens);let i=t(n,s);return s.hooks&&(i=s.hooks.postprocess(i)),i}catch(e){return o(e)}}}function Ke(e,t,r){return Je(Ue.lex,We.parse)(e,t,r)}Ke.options=Ke.setOptions=function(e){var t;return Ke.defaults={...Ke.defaults,...e},t=Ke.defaults,he=t,Ke},Ke.getDefaults=function(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}},Ke.defaults=he,Ke.use=function(...e){const t=Ke.defaults.extensions||{renderers:{},childTokens:{}};e.forEach((e=>{const r={...e};if(r.async=Ke.defaults.async||r.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if(e.renderer){const r=t.renderers[e.name];t.renderers[e.name]=r?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=r.apply(this,t)),s}:e.renderer}if(e.tokenizer){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");t[e.level]?t[e.level].unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),r.extensions=t),e.renderer){const t=Ke.defaults.renderer||new ze;for(const r in e.renderer){const s=t[r];t[r]=(...n)=>{let i=e.renderer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.renderer=t}if(e.tokenizer){const t=Ke.defaults.tokenizer||new Le;for(const r in e.tokenizer){const s=t[r];t[r]=(...n)=>{let i=e.tokenizer[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.tokenizer=t}if(e.hooks){const t=Ke.defaults.hooks||new Ge;for(const r in e.hooks){const s=t[r];Ge.passThroughHooks.has(r)?t[r]=n=>{if(Ke.defaults.async)return Promise.resolve(e.hooks[r].call(t,n)).then((e=>s.call(t,e)));const i=e.hooks[r].call(t,n);return s.call(t,i)}:t[r]=(...n)=>{let i=e.hooks[r].apply(t,n);return!1===i&&(i=s.apply(t,n)),i}}r.hooks=t}if(e.walkTokens){const t=Ke.defaults.walkTokens;r.walkTokens=function(r){let s=[];return s.push(e.walkTokens.call(this,r)),t&&(s=s.concat(t.call(this,r))),s}}Ke.setOptions(r)}))},Ke.walkTokens=function(e,t){let r=[];for(const s of e)switch(r=r.concat(t.call(Ke,s)),s.type){case"table":for(const e of s.header)r=r.concat(Ke.walkTokens(e.tokens,t));for(const e of s.rows)for(const s of e)r=r.concat(Ke.walkTokens(s.tokens,t));break;case"list":r=r.concat(Ke.walkTokens(s.items,t));break;default:Ke.defaults.extensions&&Ke.defaults.extensions.childTokens&&Ke.defaults.extensions.childTokens[s.type]?Ke.defaults.extensions.childTokens[s.type].forEach((function(e){r=r.concat(Ke.walkTokens(s[e],t))})):s.tokens&&(r=r.concat(Ke.walkTokens(s.tokens,t)))}return r},Ke.parseInline=Je(Ue.lexInline,We.parseInline),Ke.Parser=We,Ke.parser=We.parse,Ke.Renderer=ze,Ke.TextRenderer=He,Ke.Lexer=Ue,Ke.lexer=Ue.lex,Ke.Tokenizer=Le,Ke.Slugger=Ve,Ke.Hooks=Ge,Ke.parse=Ke,Ke.options,Ke.setOptions,Ke.use,Ke.walkTokens,Ke.parseInline,We.parse,Ue.lex;var Ye=r(848),Xe=r.n(Ye);r(113),r(83),r(378),r(976),r(514),r(22),r(342),r(784),r(651);const Ze=c` + .hover-bg:hover { + background: var(--bg3); + } + ::selection { + background: var(--selection-bg); + color: var(--selection-fg); + } + .regular-font { + font-family:var(--font-regular); + } + .mono-font { + font-family:var(--font-mono); + } + .title { + font-size: calc(var(--font-size-small) + 18px); + font-weight: normal + } + .sub-title{ font-size: 20px; } + .req-res-title { + font-family: var(--font-regular); + font-size: calc(var(--font-size-small) + 4px); + font-weight:bold; + margin-bottom:8px; + text-align:left; + } + .tiny-title { + font-size:calc(var(--font-size-small) + 1px); + font-weight:bold; + } + .regular-font-size { font-size: var(--font-size-regular); } + .small-font-size { font-size: var(--font-size-small); } + .upper { text-transform: uppercase; } + .primary-text { color: var(--primary-color); } + .bold-text { font-weight:bold; } + .gray-text { color: var(--light-fg); } + .red-text { color: var(--red) } + .blue-text { color: var(--blue) } + .multiline { + overflow: scroll; + max-height: var(--resp-area-height, 400px); + color: var(--fg3); + } + .method-fg.put { color: var(--orange); } + .method-fg.post { color: var(--green); } + .method-fg.get { color: var(--blue); } + .method-fg.delete { color: var(--red); } + .method-fg.options, + .method-fg.head, + .method-fg.patch { + color: var(--yellow); + } + + h1 { font-family:var(--font-regular); font-size:28px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h2 { font-family:var(--font-regular); font-size:24px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h3 { font-family:var(--font-regular); font-size:18px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h4 { font-family:var(--font-regular); font-size:16px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h5 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + h6 { font-family:var(--font-regular); font-size:14px; padding-top: 10px; letter-spacing:normal; font-weight:normal; } + + h1,h2,h3,h4,h5,h5 { + margin-block-end: 0.2em; + } + p { margin-block-start: 0.5em; } + a { color: var(--blue); cursor:pointer; } + a.inactive-link { + color:var(--fg); + text-decoration: none; + cursor:text; + } + + code, + pre { + margin: 0px; + font-family: var(--font-mono); + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown, + .m-markdown-small { + display:block; + } + + .m-markdown p, + .m-markdown span { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 8px); + } + .m-markdown li { + font-size: var(--font-size-regular); + line-height:calc(var(--font-size-regular) + 10px); + } + + .m-markdown-small p, + .m-markdown-small span, + .m-markdown-small li { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 6px); + } + .m-markdown-small li { + line-height: calc(var(--font-size-small) + 8px); + } + + .m-markdown p:not(:first-child) { + margin-block-start: 24px; + } + + .m-markdown-small p:not(:first-child) { + margin-block-start: 12px; + } + .m-markdown-small p:first-child { + margin-block-start: 0; + } + + .m-markdown p, + .m-markdown-small p { + margin-block-end: 0 + } + + .m-markdown code span { + font-size:var(--font-size-mono); + } + + .m-markdown-small code, + .m-markdown code { + padding: 1px 6px; + border-radius: 2px; + color: var(--inline-code-fg); + background-color: var(--bg3); + font-size: calc(var(--font-size-mono)); + line-height: 1.2; + } + + .m-markdown-small code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown-small pre, + .m-markdown pre { + white-space: pre-wrap; + overflow-x: auto; + line-height: normal; + border-radius: 2px; + border: 1px solid var(--code-border-color); + } + + .m-markdown pre { + padding: 12px; + background-color: var(--code-bg); + color:var(--code-fg); + } + + .m-markdown-small pre { + margin-top: 4px; + padding: 2px 4px; + background-color: var(--bg3); + color: var(--fg2); + } + + .m-markdown-small pre code, + .m-markdown pre code { + border:none; + padding:0; + } + + .m-markdown pre code { + color: var(--code-fg); + background-color: var(--code-bg); + background-color: transparent; + } + + .m-markdown-small pre code { + color: var(--fg2); + background-color: var(--bg3); + } + + .m-markdown ul, + .m-markdown ol { + padding-inline-start: 30px; + } + + .m-markdown-small ul, + .m-markdown-small ol { + padding-inline-start: 20px; + } + + .m-markdown-small a, + .m-markdown a { + color:var(--blue); + } + + .m-markdown-small img, + .m-markdown img { + max-width: 100%; + } + + /* Markdown table */ + + .m-markdown-small table, + .m-markdown table { + border-spacing: 0; + margin: 10px 0; + border-collapse: separate; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: calc(var(--font-size-small) + 1px); + line-height: calc(var(--font-size-small) + 4px); + max-width: 100%; + } + + .m-markdown-small table { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 2px); + margin: 8px 0; + } + + .m-markdown-small td, + .m-markdown-small th, + .m-markdown td, + .m-markdown th { + vertical-align: top; + border-top: 1px solid var(--border-color); + line-height: calc(var(--font-size-small) + 4px); + } + + .m-markdown-small tr:first-child th, + .m-markdown tr:first-child th { + border-top: 0 none; + } + + .m-markdown th, + .m-markdown td { + padding: 10px 12px; + } + + .m-markdown-small th, + .m-markdown-small td { + padding: 8px 8px; + } + + .m-markdown th, + .m-markdown-small th { + font-weight: 600; + background-color: var(--bg2); + vertical-align: middle; + } + + .m-markdown-small table code { + font-size: calc(var(--font-size-mono) - 2px); + } + + .m-markdown table code { + font-size: calc(var(--font-size-mono) - 1px); + } + + .m-markdown blockquote, + .m-markdown-small blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + border-left: 3px solid var(--border-color); + padding: 6px 0 6px 6px; + } + .m-markdown hr{ + border: 1px solid var(--border-color); + } +`,Qe=c` +/* Button */ +.m-btn { + border-radius: var(--border-radius); + font-weight: 600; + display: inline-block; + padding: 6px 16px; + font-size: var(--font-size-small); + outline: 0; + line-height: 1; + text-align: center; + white-space: nowrap; + border: 2px solid var(--primary-color); + background-color:transparent; + user-select: none; + cursor: pointer; + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); + transition-duration: 0.75s; +} +.m-btn.primary { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.thin-border { border-width: 1px; } +.m-btn.large { padding:8px 14px; } +.m-btn.small { padding:5px 12px; } +.m-btn.tiny { padding:5px 6px; } +.m-btn.circle { border-radius: 50%; } +.m-btn:hover { + background-color: var(--primary-color); + color: var(--primary-color-invert); +} +.m-btn.nav { border: 2px solid var(--nav-accent-color); } +.m-btn.nav:hover { + background-color: var(--nav-accent-color); +} +.m-btn:disabled { + background-color: var(--bg3); + color: var(--fg3); + border-color: var(--fg3); + cursor: not-allowed; + opacity: 0.4; +} +.m-btn:active { + filter: brightness(75%); + transform: scale(0.95); + transition:scale 0s; +} +.toolbar-btn { + cursor: pointer; + padding: 4px; + margin:0 2px; + font-size: var(--font-size-small); + min-width: 50px; + color: var(--primary-color-invert); + border-radius: 2px; + border: none; + background-color: var(--primary-color); +} + +input, textarea, select, button, pre { + color:var(--fg); + outline: none; + background-color: var(--input-bg); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); +} +button { + font-family: var(--font-regular); +} + +/* Form Inputs */ +pre, +select, +textarea, +input[type="file"], +input[type="text"], +input[type="password"] { + font-family: var(--font-mono); + font-weight: 400; + font-size: var(--font-size-small); + transition: border .2s; + padding: 6px 5px; +} + +select { + font-family: var(--font-regular); + padding: 5px 30px 5px 5px; + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2212%22%20height%3D%2212%22%3E%3Cpath%20d%3D%22M10.3%203.3L6%207.6%201.7%203.3A1%201%200%2000.3%204.7l5%205a1%201%200%20001.4%200l5-5a1%201%200%2010-1.4-1.4z%22%20fill%3D%22%23777777%22%2F%3E%3C%2Fsvg%3E"); + background-position: calc(100% - 5px) center; + background-repeat: no-repeat; + background-size: 10px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + cursor: pointer; +} + +select:hover { + border-color: var(--primary-color); +} + +textarea::placeholder, +input[type="text"]::placeholder, +input[type="password"]::placeholder { + color: var(--placeholder-color); + opacity:1; +} + + +input[type="file"]{ + font-family: var(--font-regular); + padding:2px; + cursor:pointer; + border: 1px solid var(--primary-color); + min-height: calc(var(--font-size-small) + 18px); +} + +input[type="file"]::-webkit-file-upload-button { + font-family: var(--font-regular); + font-size: var(--font-size-small); + outline: none; + cursor:pointer; + padding: 3px 8px; + border: 1px solid var(--primary-color); + background-color: var(--primary-color); + color: var(--primary-color-invert); + border-radius: var(--border-radius);; + -webkit-appearance: none; +} + +pre, +textarea { + scrollbar-width: thin; + scrollbar-color: var(--border-color) var(--input-bg); +} + +pre::-webkit-scrollbar, +textarea::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +pre::-webkit-scrollbar-track, +textarea::-webkit-scrollbar-track { + background:var(--input-bg); +} + +pre::-webkit-scrollbar-thumb, +textarea::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: var(--border-color); +} + +.link { + font-size:var(--font-size-small); + text-decoration: underline; + color:var(--blue); + font-family:var(--font-mono); + margin-bottom:2px; +} + +/* Toggle Body */ +input[type="checkbox"] { + appearance: none; + display: inline-block; + background-color: var(--light-bg); + border: 1px solid var(--light-bg); + border-radius: 9px; + cursor: pointer; + height: 18px; + position: relative; + transition: border .25s .15s, box-shadow .25s .3s, padding .25s; + min-width: 36px; + width: 36px; + vertical-align: top; +} +/* Toggle Thumb */ +input[type="checkbox"]:after { + position: absolute; + background-color: var(--bg); + border: 1px solid var(--light-bg); + border-radius: 8px; + content: ''; + top: 0px; + left: 0px; + right: 16px; + display: block; + height: 16px; + transition: border .25s .15s, left .25s .1s, right .15s .175s; +} + +/* Toggle Body - Checked */ +input[type="checkbox"]:checked { + background-color: var(--green); + border-color: var(--green); +} +/* Toggle Thumb - Checked*/ +input[type="checkbox"]:checked:after { + border: 1px solid var(--green); + left: 16px; + right: 1px; + transition: border .25s, left .15s .25s, right .25s .175s; +}`,et=c` +.row, .col { + display:flex; +} +.row { + align-items:center; + flex-direction: row; +} +.col { + align-items:stretch; + flex-direction: column; +} +`,tt=c` +.m-table { + border-spacing: 0; + border-collapse: separate; + border: 1px solid var(--light-border-color); + border-radius: var(--border-radius); + margin: 0; + max-width: 100%; + direction: ltr; +} +.m-table tr:first-child td, +.m-table tr:first-child th { + border-top: 0 none; +} +.m-table td, +.m-table th { + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 4px); + padding: 4px 5px 4px; + vertical-align: top; +} + +.m-table.padded-12 td, +.m-table.padded-12 th { + padding: 12px; +} + +.m-table td:not([align]), +.m-table th:not([align]) { + text-align: left; +} + +.m-table th { + color: var(--fg2); + font-size: var(--font-size-small); + line-height: calc(var(--font-size-small) + 18px); + font-weight: 600; + letter-spacing: normal; + background-color: var(--bg2); + vertical-align: bottom; + border-bottom: 1px solid var(--light-border-color); +} + +.m-table > tbody > tr > td, +.m-table > tr > td { + border-top: 1px solid var(--light-border-color); + text-overflow: ellipsis; + overflow: hidden; +} +.table-title { + font-size:var(--font-size-small); + font-weight:bold; + vertical-align: middle; + margin: 12px 0 4px 0; +} +`,rt=c` +:host { + container-type: inline-size; +} +.only-large-screen { display:none; } +.endpoint-head .path { + display: flex; + font-family:var(--font-mono); + font-size: var(--font-size-small); + align-items: center; + overflow-wrap: break-word; + word-break: break-all; +} + +.endpoint-head .descr { + font-size: var(--font-size-small); + color:var(--light-fg); + font-weight:400; + align-items: center; + overflow-wrap: break-word; + word-break: break-all; + display:none; +} + +.m-endpoint.expanded { margin-bottom:16px; } +.m-endpoint > .endpoint-head{ + border-width:1px 1px 1px 5px; + border-style:solid; + border-color:transparent; + border-top-color:var(--light-border-color); + display:flex; + padding:6px 16px; + align-items: center; + cursor: pointer; +} +.m-endpoint > .endpoint-head.put:hover, +.m-endpoint > .endpoint-head.put.expanded { + border-color:var(--orange); + background-color:var(--light-orange); +} +.m-endpoint > .endpoint-head.post:hover, +.m-endpoint > .endpoint-head.post.expanded { + border-color:var(--green); + background-color:var(--light-green); +} +.m-endpoint > .endpoint-head.get:hover, +.m-endpoint > .endpoint-head.get.expanded { + border-color:var(--blue); + background-color:var(--light-blue); +} +.m-endpoint > .endpoint-head.delete:hover, +.m-endpoint > .endpoint-head.delete.expanded { + border-color:var(--red); + background-color:var(--light-red); +} + +.m-endpoint > .endpoint-head.head:hover, +.m-endpoint > .endpoint-head.head.expanded, +.m-endpoint > .endpoint-head.patch:hover, +.m-endpoint > .endpoint-head.patch.expanded, +.m-endpoint > .endpoint-head.options:hover, +.m-endpoint > .endpoint-head.options.expanded { + border-color:var(--yellow); + background-color:var(--light-yellow); +} + +.m-endpoint > .endpoint-head.deprecated:hover, +.m-endpoint > .endpoint-head.deprecated.expanded { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.m-endpoint .endpoint-body { + flex-wrap:wrap; + padding:16px 0px 0 0px; + border-width:0px 1px 1px 5px; + border-style:solid; + box-shadow: 0px 4px 3px -3px rgba(0, 0, 0, 0.15); +} +.m-endpoint .endpoint-body.delete{ border-color:var(--red); } +.m-endpoint .endpoint-body.put{ border-color:var(--orange); } +.m-endpoint .endpoint-body.post { border-color:var(--green); } +.m-endpoint .endpoint-body.get { border-color:var(--blue); } +.m-endpoint .endpoint-body.head, +.m-endpoint .endpoint-body.patch, +.m-endpoint .endpoint-body.options { + border-color:var(--yellow); +} + +.m-endpoint .endpoint-body.deprecated { + border-color:var(--border-color); + filter:opacity(0.6); +} + +.endpoint-head .deprecated { + color: var(--light-fg); + filter:opacity(0.6); +} + +.summary{ + padding:8px 8px; +} +.summary .title { + font-size:calc(var(--font-size-regular) + 2px); + margin-bottom: 6px; + word-break: break-all; +} + +.endpoint-head .method { + padding:2px 5px; + vertical-align: middle; + font-size:var(--font-size-small); + height: calc(var(--font-size-small) + 16px); + line-height: calc(var(--font-size-small) + 8px); + width: 60px; + border-radius: 2px; + display:inline-block; + text-align: center; + font-weight: bold; + text-transform:uppercase; + margin-right:5px; +} +.endpoint-head .method.delete{ border: 2px solid var(--red);} +.endpoint-head .method.put{ border: 2px solid var(--orange); } +.endpoint-head .method.post{ border: 2px solid var(--green); } +.endpoint-head .method.get{ border: 2px solid var(--blue); } +.endpoint-head .method.get.deprecated{ border: 2px solid var(--border-color); } +.endpoint-head .method.head, +.endpoint-head .method.patch, +.endpoint-head .method.options { + border: 2px solid var(--yellow); +} + +.req-resp-container { + display: flex; + margin-top:16px; + align-items: stretch; + flex-wrap: wrap; + flex-direction: column; + border-top:1px solid var(--light-border-color); +} + +.view-mode-request, +api-response.view-mode { + flex:1; + min-height:100px; + padding:16px 8px; + overflow:hidden; +} +.view-mode-request { + border-width:0 0 1px 0; + border-style:dashed; +} + +.head .view-mode-request, +.patch .view-mode-request, +.options .view-mode-request { + border-color:var(--yellow); +} +.put .view-mode-request { + border-color:var(--orange); +} +.post .view-mode-request { + border-color:var(--green); +} +.get .view-mode-request { + border-color:var(--blue); +} +.delete .view-mode-request { + border-color:var(--red); +} + +@container (min-width: 1024px) { + .only-large-screen { display:block; } + .endpoint-head .path{ + font-size: var(--font-size-regular); + } + .endpoint-head .descr{ + display: flex; + } + .endpoint-head .m-markdown-small, + .descr .m-markdown-small{ + display:block; + } + .req-resp-container{ + flex-direction: var(--layout, row); + flex-wrap: nowrap; + } + api-response.view-mode { + padding:16px; + } + .view-mode-request.row-layout { + border-width:0 1px 0 0; + padding:16px; + } + .summary{ + padding:8px 16px; + } +} +`,st=c` +code[class*="language-"], +pre[class*="language-"] { + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + word-wrap: normal; + line-height: 1.5; + tab-size: 2; + + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Code blocks */ +pre[class*="language-"] { + padding: 1em; + margin: .5em 0; + overflow: auto; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + white-space: normal; +} + +.token.comment, +.token.block-comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: var(--light-fg) +} + +.token.punctuation { + color: var(--fg); +} + +.token.tag, +.token.attr-name, +.token.namespace, +.token.deleted { + color:var(--pink); +} + +.token.function-name { + color: var(--blue); +} + +.token.boolean, +.token.number, +.token.function { + color: var(--red); +} + +.token.property, +.token.class-name, +.token.constant, +.token.symbol { + color: var(--code-property-color); +} + +.token.selector, +.token.important, +.token.atrule, +.token.keyword, +.token.builtin { + color: var(--code-keyword-color); +} + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.variable { + color: var(--green); +} + +.token.operator, +.token.entity, +.token.url { + color: var(--code-operator-color); +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.inserted { + color: green; +} +`,nt=c` +.tab-panel { + border: none; +} +.tab-buttons { + height:30px; + padding: 4px 4px 0 4px; + border-bottom: 1px solid var(--light-border-color) ; + align-items: stretch; + overflow-y: hidden; + overflow-x: auto; + scrollbar-width: thin; +} +.tab-buttons::-webkit-scrollbar { + height: 1px; + background-color: var(--border-color); +} +.tab-btn { + border: none; + border-bottom: 3px solid transparent; + color: var(--light-fg); + background-color: transparent; + white-space: nowrap; + cursor:pointer; + outline:none; + font-family:var(--font-regular); + font-size:var(--font-size-small); + margin-right:16px; + padding:1px; +} +.tab-btn.active { + border-bottom: 3px solid var(--primary-color); + font-weight:bold; + color:var(--primary-color); +} + +.tab-btn:hover { + color:var(--primary-color); +} +.tab-content { + margin:-1px 0 0 0; + position:relative; + min-height: 50px; +} +`,it=c` +.nav-bar-info:focus-visible, +.nav-bar-tag:focus-visible, +.nav-bar-path:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: -4px; +} +.nav-bar-expand-all:focus-visible, +.nav-bar-collapse-all:focus-visible, +.nav-bar-tag-icon:focus-visible { + outline: 1px solid; + box-shadow: none; + outline-offset: 2px; +} +.nav-bar { + width:0; + height:100%; + overflow: hidden; + color:var(--nav-text-color); + background-color: var(--nav-bg-color); + background-blend-mode: multiply; + line-height: calc(var(--font-size-small) + 4px); + display:none; + position:relative; + flex-direction:column; + flex-wrap:nowrap; + word-break:break-word; +} +::slotted([slot=nav-logo]) { + padding:16px 16px 0 16px; +} +.nav-scroll { + overflow-x: hidden; + overflow-y: auto; + overflow-y: overlay; + scrollbar-width: thin; + scrollbar-color: var(--nav-hover-bg-color) transparent; +} + +.nav-bar-tag { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; +} +.nav-bar.read .nav-bar-tag-icon { + display:none; +} +.nav-bar-paths-under-tag { + overflow:hidden; + transition: max-height .2s ease-out, visibility .3s; +} +.collapsed .nav-bar-paths-under-tag { + visibility: hidden; +} + +.nav-bar-expand-all { + transform: rotate(90deg); + cursor:pointer; + margin-right:10px; +} +.nav-bar-collapse-all { + transform: rotate(270deg); + cursor:pointer; +} +.nav-bar-expand-all:hover, .nav-bar-collapse-all:hover { + color: var(--primary-color); +} + +.nav-bar-tag-icon { + color: var(--nav-text-color); + font-size: 20px; +} +.nav-bar-tag-icon:hover { + color:var(--nav-hover-text-color); +} +.nav-bar.focused .nav-bar-tag-and-paths.collapsed .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transform: rotate(-90deg); + transition: transform 0.2s ease-out 0s; +} +.nav-bar.focused .nav-bar-tag-and-paths.expanded .nav-bar-tag-icon::after { + content: '⌵'; + width:16px; + height:16px; + text-align: center; + display: inline-block; + transition: transform 0.2s ease-out 0s; +} +.nav-scroll::-webkit-scrollbar { + width: var(--scroll-bar-width, 8px); +} +.nav-scroll::-webkit-scrollbar-track { + background:transparent; +} +.nav-scroll::-webkit-scrollbar-thumb { + background-color: var(--nav-hover-bg-color); +} + +.nav-bar-tag { + font-size: var(--font-size-regular); + color: var(--nav-accent-color); + border-left:4px solid transparent; + font-weight:bold; + padding: 15px 15px 15px 10px; + text-transform: capitalize; +} + +.nav-bar-components, +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-info, +.nav-bar-tag, +.nav-bar-path { + display:flex; + cursor: pointer; + width: 100%; + border: none; + border-radius:4px; + color: var(--nav-text-color); + background: transparent; + border-left:4px solid transparent; +} + +.nav-bar-h1, +.nav-bar-h2, +.nav-bar-path { + font-size: calc(var(--font-size-small) + 1px); + padding: var(--nav-item-padding); +} +.nav-bar-path.small-font { + font-size: var(--font-size-small); +} + +.nav-bar-info { + font-size: var(--font-size-regular); + padding: 16px 10px; + font-weight:bold; +} +.nav-bar-section { + display: flex; + flex-direction: row; + justify-content: space-between; + font-size: var(--font-size-small); + color: var(--nav-text-color); + padding: var(--nav-item-padding); + font-weight:bold; +} +.nav-bar-section.operations { + cursor:pointer; +} +.nav-bar-section.operations:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} + +.nav-bar-section:first-child { + display: none; +} +.nav-bar-h2 {margin-left:12px;} + +.nav-bar-h1.left-bar.active, +.nav-bar-h2.left-bar.active, +.nav-bar-info.left-bar.active, +.nav-bar-tag.left-bar.active, +.nav-bar-path.left-bar.active, +.nav-bar-section.left-bar.operations.active { + border-left:4px solid var(--nav-accent-color); + color:var(--nav-hover-text-color); +} + +.nav-bar-h1.colored-block.active, +.nav-bar-h2.colored-block.active, +.nav-bar-info.colored-block.active, +.nav-bar-tag.colored-block.active, +.nav-bar-path.colored-block.active, +.nav-bar-section.colored-block.operations.active { + background-color: var(--nav-accent-color); + color: var(--nav-accent-text-color); + border-radius: 0; +} + +.nav-bar-h1:hover, +.nav-bar-h2:hover, +.nav-bar-info:hover, +.nav-bar-tag:hover, +.nav-bar-path:hover { + color:var(--nav-hover-text-color); + background-color:var(--nav-hover-bg-color); +} +`,ot=c` +#api-info { + font-size: calc(var(--font-size-regular) - 1px); + margin-top: 8px; + margin-left: -15px; +} + +#api-info span:before { + content: "|"; + display: inline-block; + opacity: 0.5; + width: 15px; + text-align: center; +} +#api-info span:first-child:before { + content: ""; + width: 0px; +} +`,at=c` + +`,lt=/[\s#:?&={}]/g,ct="_rapidoc_api_key";function pt(e){return new Promise((t=>setTimeout(t,e)))}function ut(e,t){const r=t.target,s=document.createElement("textarea");s.value=e,s.style.position="fixed",document.body.appendChild(s),s.focus(),s.select();try{document.execCommand("copy"),r.innerText="Copied",setTimeout((()=>{r.innerText="Copy"}),5e3)}catch(e){console.error("Unable to copy",e)}document.body.removeChild(s)}function dt(e,t,r=""){return`${t.method} ${t.path} ${t.summary||""} ${t.description||""} ${t.operationId||""} ${r}`.toLowerCase().includes(e.toLowerCase())}function ht(e,t=new Set){return e?(Object.keys(e).forEach((r=>{var s;if(t.add(r),e[r].properties)ht(e[r].properties,t);else if(null!==(s=e[r].items)&&void 0!==s&&s.properties){var n;ht(null===(n=e[r].items)||void 0===n?void 0:n.properties,t)}})),t):t}function mt(e,t){if(e){const r=document.createElement("a");document.body.appendChild(r),r.style="display: none",r.href=e,r.download=t,r.click(),r.remove()}}function ft(e){if(e){const t=document.createElement("a");document.body.appendChild(t),t.style="display: none",t.href=e,t.target="_blank",t.click(),t.remove()}}const gt=Object.freeze({url:"/"}),{fetch:yt,Response:vt,Headers:bt,Request:xt,FormData:wt,File:$t,Blob:St}=globalThis;function Et(e,t){return t||"undefined"==typeof navigator||(t=navigator),t&&"ReactNative"===t.product?!(!e||"object"!=typeof e||"string"!=typeof e.uri):"undefined"!=typeof File&&e instanceof File||"undefined"!=typeof Blob&&e instanceof Blob||!!ArrayBuffer.isView(e)||null!==e&&"object"==typeof e&&"function"==typeof e.pipe}function kt(e,t){return Array.isArray(e)&&e.some((e=>Et(e,t)))}void 0===globalThis.fetch&&(globalThis.fetch=yt),void 0===globalThis.Headers&&(globalThis.Headers=bt),void 0===globalThis.Request&&(globalThis.Request=xt),void 0===globalThis.Response&&(globalThis.Response=vt),void 0===globalThis.FormData&&(globalThis.FormData=wt),void 0===globalThis.File&&(globalThis.File=$t),void 0===globalThis.Blob&&(globalThis.Blob=St);class At extends File{constructor(e,t="",r={}){super([e],t,r),this.data=e}valueOf(){return this.data}toString(){return this.valueOf()}}function Ot(e,t="reserved"){return[...e].map((e=>{if((e=>/^[a-z0-9\-._~]+$/i.test(e))(e))return e;if((e=>":/?#[]@!$&'()*+,;=".indexOf(e)>-1)(e)&&"unsafe"===t)return e;const r=new TextEncoder;return Array.from(r.encode(e)).map((e=>`0${e.toString(16).toUpperCase()}`.slice(-2))).map((e=>`%${e}`)).join("")})).join("")}function jt(e){const{value:t}=e;return Array.isArray(t)?function({key:e,value:t,style:r,explode:s,escape:n}){if("simple"===r)return t.map((e=>Tt(e,n))).join(",");if("label"===r)return`.${t.map((e=>Tt(e,n))).join(".")}`;if("matrix"===r)return t.map((e=>Tt(e,n))).reduce(((t,r)=>!t||s?`${t||""};${e}=${r}`:`${t},${r}`),"");if("form"===r){const r=s?`&${e}=`:",";return t.map((e=>Tt(e,n))).join(r)}if("spaceDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Tt(e,n))).join(` ${r}`)}if("pipeDelimited"===r){const r=s?`${e}=`:"";return t.map((e=>Tt(e,n))).join(`|${r}`)}}(e):"object"==typeof t?function({key:e,value:t,style:r,explode:s,escape:n}){const i=Object.keys(t);return"simple"===r?i.reduce(((e,r)=>{const i=Tt(t[r],n);return`${e?`${e},`:""}${r}${s?"=":","}${i}`}),""):"label"===r?i.reduce(((e,r)=>{const i=Tt(t[r],n);return`${e?`${e}.`:"."}${r}${s?"=":"."}${i}`}),""):"matrix"===r&&s?i.reduce(((e,r)=>`${e?`${e};`:";"}${r}=${Tt(t[r],n)}`),""):"matrix"===r?i.reduce(((r,s)=>{const i=Tt(t[s],n);return`${r?`${r},`:`;${e}=`}${s},${i}`}),""):"form"===r?i.reduce(((e,r)=>{const i=Tt(t[r],n);return`${e?`${e}${s?"&":","}`:""}${r}${s?"=":","}${i}`}),""):void 0}(e):function({key:e,value:t,style:r,escape:s}){return"simple"===r?Tt(t,s):"label"===r?`.${Tt(t,s)}`:"matrix"===r?`;${e}=${Tt(t,s)}`:"form"===r||"deepObject"===r?Tt(t,s):void 0}(e)}function Tt(e,t=!1){return Array.isArray(e)||null!==e&&"object"==typeof e?e=JSON.stringify(e):"number"!=typeof e&&"boolean"!=typeof e||(e=String(e)),t&&e.length>0?Ot(e,t):e}const Pt={form:",",spaceDelimited:"%20",pipeDelimited:"|"},Ct={csv:",",ssv:"%20",tsv:"%09",pipes:"|"};function It(e,t,r=!1){const{collectionFormat:s,allowEmptyValue:n,serializationOption:i,encoding:o}=t,a="object"!=typeof t||Array.isArray(t)?t:t.value,l=r?e=>e.toString():e=>encodeURIComponent(e),c=l(e);if(void 0===a&&n)return[[c,""]];if(Et(a)||kt(a))return[[c,a]];if(i)return _t(e,a,r,i);if(o){if([typeof o.style,typeof o.explode,typeof o.allowReserved].some((e=>"undefined"!==e))){const{style:t,explode:s,allowReserved:n}=o;return _t(e,a,r,{style:t,explode:s,allowReserved:n})}if("string"==typeof o.contentType){if(o.contentType.startsWith("application/json")){const e=l("string"==typeof a?a:JSON.stringify(a));return[[c,new At(e,"blob",{type:o.contentType})]]}const e=l(String(a));return[[c,new At(e,"blob",{type:o.contentType})]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)&&a.every((e=>"object"!=typeof e))?[[c,a.map(l).join(",")]]:[[c,l(JSON.stringify(a))]]}return"object"!=typeof a?[[c,l(a)]]:Array.isArray(a)?"multi"===s?[[c,a.map(l)]]:[[c,a.map(l).join(Ct[s||"csv"])]]:[[c,""]]}function _t(e,t,r,s){const n=s.style||"form",i=void 0===s.explode?"form"===n:s.explode,o=!r&&(s&&s.allowReserved?"unsafe":"reserved"),a=e=>Tt(e,o),l=r?e=>e:e=>a(e);return"object"!=typeof t?[[l(e),a(t)]]:Array.isArray(t)?i?[[l(e),t.map(a)]]:[[l(e),t.map(a).join(Pt[n])]]:"deepObject"===n?Object.keys(t).map((r=>[l(`${e}[${r}]`),a(t[r])])):i?Object.keys(t).map((e=>[l(e),a(t[e])])):[[l(e),Object.keys(t).map((e=>[`${l(e)},${a(t[e])}`])).join(",")]]}function Rt(e){return((e,{encode:t=!0}={})=>{const r=(e,t,s)=>(null==s?e.append(t,""):Array.isArray(s)?s.reduce(((s,n)=>r(e,t,n)),e):s instanceof Date?e.append(t,s.toISOString()):"object"==typeof s?Object.entries(s).reduce(((s,[n,i])=>r(e,`${t}[${n}]`,i)),e):e.append(t,s),e),s=Object.entries(e).reduce(((e,[t,s])=>r(e,t,s)),new URLSearchParams),n=String(s);return t?n:decodeURIComponent(n)})(Object.keys(e).reduce(((t,r)=>{for(const[s,n]of It(r,e[r]))t[s]=n instanceof At?n.valueOf():n;return t}),{}),{encode:!1})}function Ft(e={}){const{url:t="",query:r,form:s}=e;if(s){const t=Object.keys(s).some((e=>{const{value:t}=s[e];return Et(t)||kt(t)})),r=e.headers["content-type"]||e.headers["Content-Type"];if(t||/multipart\/form-data/i.test(r)){const t=(n=e.form,Object.entries(n).reduce(((e,[t,r])=>{for(const[s,n]of It(t,r,!0))if(Array.isArray(n))for(const t of n)if(ArrayBuffer.isView(t)){const r=new Blob([t]);e.append(s,r)}else e.append(s,t);else if(ArrayBuffer.isView(n)){const t=new Blob([n]);e.append(s,t)}else e.append(s,n);return e}),new FormData));e.formdata=t,e.body=t}else e.body=Rt(s);delete e.form}var n;if(r){const[s,n]=t.split("?");let i="";if(n){const e=new URLSearchParams(n);Object.keys(r).forEach((t=>e.delete(t))),i=String(e)}const o=((...e)=>{const t=e.filter((e=>e)).join("&");return t?`?${t}`:""})(i,Rt(r));e.url=s+o,delete e.query}return e}function Mt(e){return null==e}var Lt={isNothing:Mt,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:Mt(e)?[]:[e]},repeat:function(e,t){var r,s="";for(r=0;ra&&(t=s-a+(i=" ... ").length),r-s>a&&(r=s+a-(o=" ...").length),{str:i+e.slice(t,r).replace(/\t/g,"→")+o,pos:s-t+i.length}}function Ut(e,t){return Lt.repeat(" ",t-e.length)+e}var zt=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var r,s=/\r?\n|\r|\0/g,n=[0],i=[],o=-1;r=s.exec(e.buffer);)i.push(r.index),n.push(r.index+r[0].length),e.position<=r.index&&o<0&&(o=n.length-2);o<0&&(o=n.length-1);var a,l,c="",p=Math.min(e.line+t.linesAfter,i.length).toString().length,u=t.maxLength-(t.indent+p+3);for(a=1;a<=t.linesBefore&&!(o-a<0);a++)l=Nt(e.buffer,n[o-a],i[o-a],e.position-(n[o]-n[o-a]),u),c=Lt.repeat(" ",t.indent)+Ut((e.line-a+1).toString(),p)+" | "+l.str+"\n"+c;for(l=Nt(e.buffer,n[o],i[o],e.position,u),c+=Lt.repeat(" ",t.indent)+Ut((e.line+1).toString(),p)+" | "+l.str+"\n",c+=Lt.repeat("-",t.indent+p+3+l.pos)+"^\n",a=1;a<=t.linesAfter&&!(o+a>=i.length);a++)l=Nt(e.buffer,n[o+a],i[o+a],e.position-(n[o]-n[o+a]),u),c+=Lt.repeat(" ",t.indent)+Ut((e.line+a+1).toString(),p)+" | "+l.str+"\n";return c.replace(/\n$/,"")},Ht=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],Vt=["scalar","sequence","mapping"],Wt=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===Ht.indexOf(t))throw new qt('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(r){e[r].forEach((function(e){t[String(e)]=r}))})),t}(t.styleAliases||null),-1===Vt.indexOf(this.kind))throw new qt('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function Gt(e,t){var r=[];return e[t].forEach((function(e){var t=r.length;r.forEach((function(r,s){r.tag===e.tag&&r.kind===e.kind&&r.multi===e.multi&&(t=s)})),r[t]=e})),r}function Jt(e){return this.extend(e)}Jt.prototype.extend=function(e){var t=[],r=[];if(e instanceof Wt)r.push(e);else if(Array.isArray(e))r=r.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new qt("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(r=r.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof Wt))throw new qt("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new qt("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new qt("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),r.forEach((function(e){if(!(e instanceof Wt))throw new qt("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var s=Object.create(Jt.prototype);return s.implicit=(this.implicit||[]).concat(t),s.explicit=(this.explicit||[]).concat(r),s.compiledImplicit=Gt(s,"implicit"),s.compiledExplicit=Gt(s,"explicit"),s.compiledTypeMap=function(){var e,t,r={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function s(e){e.multi?(r.multi[e.kind].push(e),r.multi.fallback.push(e)):r[e.kind][e.tag]=r.fallback[e.tag]=e}for(e=0,t=arguments.length;e=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0o"+e.toString(8):"-0o"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}}),ir=new RegExp("^(?:[-+]?(?:[0-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),or=/^[-+]?[0-9]+e/,ar=new Wt("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!ir.test(e)||"_"===e[e.length-1])},construct:function(e){var t,r;return r="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===r?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:r*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||Lt.isNegativeZero(e))},represent:function(e,t){var r;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(Lt.isNegativeZero(e))return"-0.0";return r=e.toString(10),or.test(r)?r.replace("e",".e"):r},defaultStyle:"lowercase"}),lr=Qt.extend({implicit:[er,tr,nr,ar]}),cr=lr,pr=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),ur=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$"),dr=new Wt("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:function(e){return null!==e&&(null!==pr.exec(e)||null!==ur.exec(e))},construct:function(e){var t,r,s,n,i,o,a,l,c=0,p=null;if(null===(t=pr.exec(e))&&(t=ur.exec(e)),null===t)throw new Error("Date resolve error");if(r=+t[1],s=+t[2]-1,n=+t[3],!t[4])return new Date(Date.UTC(r,s,n));if(i=+t[4],o=+t[5],a=+t[6],t[7]){for(c=t[7].slice(0,3);c.length<3;)c+="0";c=+c}return t[9]&&(p=6e4*(60*+t[10]+ +(t[11]||0)),"-"===t[9]&&(p=-p)),l=new Date(Date.UTC(r,s,n,i,o,a,c)),p&&l.setTime(l.getTime()-p),l},instanceOf:Date,represent:function(e){return e.toISOString()}}),hr=new Wt("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}}),mr="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\n\r",fr=new Wt("tag:yaml.org,2002:binary",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,r,s=0,n=e.length,i=mr;for(r=0;r64)){if(t<0)return!1;s+=6}return s%8==0},construct:function(e){var t,r,s=e.replace(/[\r\n=]/g,""),n=s.length,i=mr,o=0,a=[];for(t=0;t>16&255),a.push(o>>8&255),a.push(255&o)),o=o<<6|i.indexOf(s.charAt(t));return 0==(r=n%4*6)?(a.push(o>>16&255),a.push(o>>8&255),a.push(255&o)):18===r?(a.push(o>>10&255),a.push(o>>2&255)):12===r&&a.push(o>>4&255),new Uint8Array(a)},predicate:function(e){return"[object Uint8Array]"===Object.prototype.toString.call(e)},represent:function(e){var t,r,s="",n=0,i=e.length,o=mr;for(t=0;t>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]),n=(n<<8)+e[t];return 0==(r=i%3)?(s+=o[n>>18&63],s+=o[n>>12&63],s+=o[n>>6&63],s+=o[63&n]):2===r?(s+=o[n>>10&63],s+=o[n>>4&63],s+=o[n<<2&63],s+=o[64]):1===r&&(s+=o[n>>2&63],s+=o[n<<4&63],s+=o[64],s+=o[64]),s}}),gr=Object.prototype.hasOwnProperty,yr=Object.prototype.toString,vr=new Wt("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,r,s,n,i,o=[],a=e;for(t=0,r=a.length;t>10),56320+(e-65536&1023))}for(var Dr=new Array(256),Br=new Array(256),qr=0;qr<256;qr++)Dr[qr]=Mr(qr)?1:0,Br[qr]=Mr(qr);function Nr(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||Sr,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.firstTabInLine=-1,this.documents=[]}function Ur(e,t){var r={name:e.filename,buffer:e.input.slice(0,-1),position:e.position,line:e.line,column:e.position-e.lineStart};return r.snippet=zt(r),new qt(t,r)}function zr(e,t){throw Ur(e,t)}function Hr(e,t){e.onWarning&&e.onWarning.call(null,Ur(e,t))}var Vr={YAML:function(e,t,r){var s,n,i;null!==e.version&&zr(e,"duplication of %YAML directive"),1!==r.length&&zr(e,"YAML directive accepts exactly one argument"),null===(s=/^([0-9]+)\.([0-9]+)$/.exec(r[0]))&&zr(e,"ill-formed argument of the YAML directive"),n=parseInt(s[1],10),i=parseInt(s[2],10),1!==n&&zr(e,"unacceptable YAML version of the document"),e.version=r[0],e.checkLineBreaks=i<2,1!==i&&2!==i&&Hr(e,"unsupported YAML version of the document")},TAG:function(e,t,r){var s,n;2!==r.length&&zr(e,"TAG directive accepts exactly two arguments"),s=r[0],n=r[1],jr.test(s)||zr(e,"ill-formed tag handle (first argument) of the TAG directive"),Er.call(e.tagMap,s)&&zr(e,'there is a previously declared suffix for "'+s+'" tag handle'),Tr.test(n)||zr(e,"ill-formed tag prefix (second argument) of the TAG directive");try{n=decodeURIComponent(n)}catch(t){zr(e,"tag prefix is malformed: "+n)}e.tagMap[s]=n}};function Wr(e,t,r,s){var n,i,o,a;if(t1&&(e.result+=Lt.repeat("\n",t-1))}function Qr(e,t){var r,s,n=e.tag,i=e.anchor,o=[],a=!1;if(-1!==e.firstTabInLine)return!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=o),s=e.input.charCodeAt(e.position);0!==s&&(-1!==e.firstTabInLine&&(e.position=e.firstTabInLine,zr(e,"tab characters must not be used in indentation")),45===s)&&_r(e.input.charCodeAt(e.position+1));)if(a=!0,e.position++,Yr(e,!0,-1)&&e.lineIndent<=t)o.push(null),s=e.input.charCodeAt(e.position);else if(r=e.line,rs(e,t,3,!1,!0),o.push(e.result),Yr(e,!0,-1),s=e.input.charCodeAt(e.position),(e.line===r||e.lineIndent>t)&&0!==s)zr(e,"bad indentation of a sequence entry");else if(e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt?m=1:e.lineIndent===t?m=0:e.lineIndentt)&&(y&&(o=e.line,a=e.lineStart,l=e.position),rs(e,t,4,!0,n)&&(y?f=e.result:g=e.result),y||(Jr(e,d,h,m,f,g,o,a,l),m=f=g=null),Yr(e,!0,-1),c=e.input.charCodeAt(e.position)),(e.line===i||e.lineIndent>t)&&0!==c)zr(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===n?zr(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):c?zr(e,"repeat of an indentation width identifier"):(p=t+n-1,c=!0)}if(Ir(i)){do{i=e.input.charCodeAt(++e.position)}while(Ir(i));if(35===i)do{i=e.input.charCodeAt(++e.position)}while(!Cr(i)&&0!==i)}for(;0!==i;){for(Kr(e),e.lineIndent=0,i=e.input.charCodeAt(e.position);(!c||e.lineIndentp&&(p=e.lineIndent),Cr(i))u++;else{if(e.lineIndent0){for(n=o,i=0;n>0;n--)(o=Fr(a=e.input.charCodeAt(++e.position)))>=0?i=(i<<4)+o:zr(e,"expected hexadecimal character");e.result+=Lr(i),e.position++}else zr(e,"unknown escape sequence");r=s=e.position}else Cr(a)?(Wr(e,r,s,!0),Zr(e,Yr(e,!1,t)),r=s=e.position):e.position===e.lineStart&&Xr(e)?zr(e,"unexpected end of the document within a double quoted scalar"):(e.position++,s=e.position)}zr(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?g=!0:function(e){var t,r,s;if(42!==(s=e.input.charCodeAt(e.position)))return!1;for(s=e.input.charCodeAt(++e.position),t=e.position;0!==s&&!_r(s)&&!Rr(s);)s=e.input.charCodeAt(++e.position);return e.position===t&&zr(e,"name of an alias node must contain at least one character"),r=e.input.slice(t,e.position),Er.call(e.anchorMap,r)||zr(e,'unidentified alias "'+r+'"'),e.result=e.anchorMap[r],Yr(e,!0,-1),!0}(e)?(g=!0,null===e.tag&&null===e.anchor||zr(e,"alias node should not have any properties")):function(e,t,r){var s,n,i,o,a,l,c,p,u=e.kind,d=e.result;if(_r(p=e.input.charCodeAt(e.position))||Rr(p)||35===p||38===p||42===p||33===p||124===p||62===p||39===p||34===p||37===p||64===p||96===p)return!1;if((63===p||45===p)&&(_r(s=e.input.charCodeAt(e.position+1))||r&&Rr(s)))return!1;for(e.kind="scalar",e.result="",n=i=e.position,o=!1;0!==p;){if(58===p){if(_r(s=e.input.charCodeAt(e.position+1))||r&&Rr(s))break}else if(35===p){if(_r(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&Xr(e)||r&&Rr(p))break;if(Cr(p)){if(a=e.line,l=e.lineStart,c=e.lineIndent,Yr(e,!1,-1),e.lineIndent>=t){o=!0,p=e.input.charCodeAt(e.position);continue}e.position=i,e.line=a,e.lineStart=l,e.lineIndent=c;break}}o&&(Wr(e,n,i,!1),Zr(e,e.line-a),n=i=e.position,o=!1),Ir(p)||(i=e.position+1),p=e.input.charCodeAt(++e.position)}return Wr(e,n,i,!1),!!e.result||(e.kind=u,e.result=d,!1)}(e,d,1===r)&&(g=!0,null===e.tag&&(e.tag="?")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===m&&(g=a&&Qr(e,h))),null===e.tag)null!==e.anchor&&(e.anchorMap[e.anchor]=e.result);else if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&zr(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),l=0,c=e.implicitTypes.length;l"),null!==e.result&&u.kind!==e.kind&&zr(e,"unacceptable node kind for !<"+e.tag+'> tag; it should be "'+u.kind+'", not "'+e.kind+'"'),u.resolve(e.result,e.tag)?(e.result=u.construct(e.result,e.tag),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):zr(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")}return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||g}function ss(e){var t,r,s,n,i=e.position,o=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap=Object.create(null),e.anchorMap=Object.create(null);0!==(n=e.input.charCodeAt(e.position))&&(Yr(e,!0,-1),n=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==n));){for(o=!0,n=e.input.charCodeAt(++e.position),t=e.position;0!==n&&!_r(n);)n=e.input.charCodeAt(++e.position);for(s=[],(r=e.input.slice(t,e.position)).length<1&&zr(e,"directive name must not be less than one character in length");0!==n;){for(;Ir(n);)n=e.input.charCodeAt(++e.position);if(35===n){do{n=e.input.charCodeAt(++e.position)}while(0!==n&&!Cr(n));break}if(Cr(n))break;for(t=e.position;0!==n&&!_r(n);)n=e.input.charCodeAt(++e.position);s.push(e.input.slice(t,e.position))}0!==n&&Kr(e),Er.call(Vr,r)?Vr[r](e,r,s):Hr(e,'unknown document directive "'+r+'"')}Yr(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,Yr(e,!0,-1)):o&&zr(e,"directives end mark is expected"),rs(e,e.lineIndent-1,4,!1,!0),Yr(e,!0,-1),e.checkLineBreaks&&Ar.test(e.input.slice(i,e.position))&&Hr(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&Xr(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,Yr(e,!0,-1)):e.position=55296&&s<=56319&&t+1=56320&&r<=57343?1024*(s-55296)+r-56320+65536:s}function bs(e){return/^\n* /.test(e)}function xs(e,t){var r=bs(e)?String(t):"",s="\n"===e[e.length-1];return r+(!s||"\n"!==e[e.length-2]&&"\n"!==e?s?"":"-":"+")+"\n"}function ws(e){return"\n"===e[e.length-1]?e.slice(0,-1):e}function $s(e,t){if(""===e||" "===e[0])return e;for(var r,s,n=/ [^ ]/g,i=0,o=0,a=0,l="";r=n.exec(e);)(a=r.index)-i>t&&(s=o>i?o:a,l+="\n"+e.slice(i,s),i=s+1),o=a;return l+="\n",e.length-i>t&&o>i?l+=e.slice(i,o)+"\n"+e.slice(o+1):l+=e.slice(i),l.slice(1)}function Ss(e,t,r,s){var n,i,o,a="",l=e.tag;for(n=0,i=r.length;n tag resolver accepts not "'+l+'" style');s=a.represent[l](t,l)}e.dump=s}return!0}return!1}function ks(e,t,r,s,n,i,o){e.tag=null,e.dump=r,Es(e,r,!1)||Es(e,r,!0);var a,l=is.call(e.dump),c=s;s&&(s=e.flowLevel<0||e.flowLevel>t);var p,u,d="[object Object]"===l||"[object Array]"===l;if(d&&(u=-1!==(p=e.duplicates.indexOf(r))),(null!==e.tag&&"?"!==e.tag||u||2!==e.indent&&t>0)&&(n=!1),u&&e.usedDuplicates[p])e.dump="*ref_"+p;else{if(d&&u&&!e.usedDuplicates[p]&&(e.usedDuplicates[p]=!0),"[object Object]"===l)s&&0!==Object.keys(e.dump).length?(function(e,t,r,s){var n,i,o,a,l,c,p="",u=e.tag,d=Object.keys(r);if(!0===e.sortKeys)d.sort();else if("function"==typeof e.sortKeys)d.sort(e.sortKeys);else if(e.sortKeys)throw new qt("sortKeys must be a boolean or a function");for(n=0,i=d.length;n1024)&&(e.dump&&10===e.dump.charCodeAt(0)?c+="?":c+="? "),c+=e.dump,l&&(c+=hs(e,t)),ks(e,t+1,a,!0,l)&&(e.dump&&10===e.dump.charCodeAt(0)?c+=":":c+=": ",p+=c+=e.dump));e.tag=u,e.dump=p||"{}"}(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o,a,l="",c=e.tag,p=Object.keys(r);for(s=0,n=p.length;s1024&&(a+="? "),a+=e.dump+(e.condenseFlow?'"':"")+":"+(e.condenseFlow?"":" "),ks(e,t,o,!1,!1)&&(l+=a+=e.dump));e.tag=c,e.dump="{"+l+"}"}(e,t,e.dump),u&&(e.dump="&ref_"+p+" "+e.dump));else if("[object Array]"===l)s&&0!==e.dump.length?(e.noArrayIndent&&!o&&t>0?Ss(e,t-1,e.dump,n):Ss(e,t,e.dump,n),u&&(e.dump="&ref_"+p+e.dump)):(function(e,t,r){var s,n,i,o="",a=e.tag;for(s=0,n=r.length;s-1&&r>=e.flowLevel;switch(function(e,t,r,s,n,i,o,a){var l,c,p=0,u=null,d=!1,h=!1,m=-1!==s,f=-1,g=fs(c=vs(e,0))&&65279!==c&&!ms(c)&&45!==c&&63!==c&&58!==c&&44!==c&&91!==c&&93!==c&&123!==c&&125!==c&&35!==c&&38!==c&&42!==c&&33!==c&&124!==c&&61!==c&&62!==c&&39!==c&&34!==c&&37!==c&&64!==c&&96!==c&&function(e){return!ms(e)&&58!==e}(vs(e,e.length-1));if(t||o)for(l=0;l=65536?l+=2:l++){if(!fs(p=vs(e,l)))return 5;g=g&&ys(p,u,a),u=p}else{for(l=0;l=65536?l+=2:l++){if(10===(p=vs(e,l)))d=!0,m&&(h=h||l-f-1>s&&" "!==e[f+1],f=l);else if(!fs(p))return 5;g=g&&ys(p,u,a),u=p}h=h||m&&l-f-1>s&&" "!==e[f+1]}return d||h?r>9&&bs(e)?5:o?2===i?5:2:h?4:3:!g||o||n(e)?2===i?5:2:1}(t,a,e.indent,o,(function(t){return function(e,t){var r,s;for(r=0,s=e.implicitTypes.length;r"+xs(t,e.indent)+ws(ds(function(e,t){for(var r,s,n,i=/(\n+)([^\n]*)/g,o=(n=-1!==(n=e.indexOf("\n"))?n:e.length,i.lastIndex=n,$s(e.slice(0,n),t)),a="\n"===e[0]||" "===e[0];s=i.exec(e);){var l=s[1],c=s[2];r=" "===c[0],o+=l+(a||r||""===c?"":"\n")+$s(c,t),a=r}return o}(t,o),i));case 5:return'"'+function(e){for(var t,r="",s=0,n=0;n=65536?n+=2:n++)s=vs(e,n),!(t=as[s])&&fs(s)?(r+=e[n],s>=65536&&(r+=e[n+1])):r+=t||ps(s);return r}(t)+'"';default:throw new qt("impossible error: invalid scalar style")}}()}(e,e.dump,t,i,c)}null!==e.tag&&"?"!==e.tag&&(a=encodeURI("!"===e.tag[0]?e.tag.slice(1):e.tag).replace(/!/g,"%21"),a="!"===e.tag[0]?"!"+a:"tag:yaml.org,2002:"===a.slice(0,18)?"!!"+a.slice(18):"!<"+a+">",e.dump=a+" "+e.dump)}return!0}function As(e,t){var r,s,n=[],i=[];for(Os(e,n,i),r=0,s=i.length;r(e[t]=function(e){return e.includes(", ")?e.split(", "):e}(r),e)),{})}function Hs(e,t,{loadSpec:r=!1}={}){const s={ok:e.ok,url:e.url||t,status:e.status,statusText:e.statusText,headers:zs(e.headers)},n=s.headers["content-type"],i=r||((e="")=>/(json|xml|yaml|text)\b/.test(e))(n);return(i?e.text:e.blob||e.buffer).call(e).then((e=>{if(s.text=e,s.data=e,i)try{const t=function(e,t){return t&&(0===t.indexOf("application/json")||t.indexOf("+json")>0)?JSON.parse(e):Us.load(e)}(e,n);s.body=t,s.obj=t}catch(e){s.parseError=e}return s}))}async function Vs(e,t={}){"object"==typeof e&&(e=(t=e).url),t.headers=t.headers||{},(t=Ft(t)).headers&&Object.keys(t.headers).forEach((e=>{const r=t.headers[e];"string"==typeof r&&(t.headers[e]=r.replace(/\n+/g," "))})),t.requestInterceptor&&(t=await t.requestInterceptor(t)||t);const r=t.headers["content-type"]||t.headers["Content-Type"];let s;/multipart\/form-data/i.test(r)&&(delete t.headers["content-type"],delete t.headers["Content-Type"]);try{s=await(t.userFetch||fetch)(t.url,t),s=await Hs(s,e,t),t.responseInterceptor&&(s=await t.responseInterceptor(s)||s)}catch(e){if(!s)throw e;const t=new Error(s.statusText||`response status is ${s.status}`);throw t.status=s.status,t.statusCode=s.status,t.responseError=e,t}if(!s.ok){const e=new Error(s.statusText||`response status is ${s.status}`);throw e.status=s.status,e.statusCode=s.status,e.response=s,e}return s}function Ws(e,t={}){const{requestInterceptor:r,responseInterceptor:s}=t,n=e.withCredentials?"include":"same-origin";return t=>e({url:t,loadSpec:!0,requestInterceptor:r,responseInterceptor:s,headers:{Accept:"application/json, application/yaml"},credentials:n}).then((e=>e.body))}const Gs=e=>{var t,r;const{baseDoc:s,url:n}=e,i=null!==(t=null!=s?s:n)&&void 0!==t?t:"";return"string"==typeof(null===(r=globalThis.document)||void 0===r?void 0:r.baseURI)?String(new URL(i,globalThis.document.baseURI)):i},Js=e=>{const{fetch:t,http:r}=e;return t||r||Vs};var Ks,Ys=(Ks=function(e,t){return Ks=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])},Ks(e,t)},function(e,t){function r(){this.constructor=e}Ks(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}),Xs=Object.prototype.hasOwnProperty;function Zs(e,t){return Xs.call(e,t)}function Qs(e){if(Array.isArray(e)){for(var t=new Array(e.length),r=0;r=48&&t<=57))return!1;r++}return!0}function rn(e){return-1===e.indexOf("/")&&-1===e.indexOf("~")?e:e.replace(/~/g,"~0").replace(/\//g,"~1")}function sn(e){return e.replace(/~1/g,"/").replace(/~0/g,"~")}function nn(e){if(void 0===e)return!0;if(e)if(Array.isArray(e)){for(var t=0,r=e.length;t0&&"constructor"==a[c-1]))throw new TypeError("JSON-Patch: modifying `__proto__` or `constructor/prototype` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README");if(r&&void 0===u&&(void 0===l[d]?u=a.slice(0,c).join("/"):c==p-1&&(u=t.path),void 0!==u&&h(t,0,e,u)),c++,Array.isArray(l)){if("-"===d)d=l.length;else{if(r&&!tn(d))throw new ln("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index","OPERATION_PATH_ILLEGAL_ARRAY_INDEX",i,t,e);tn(d)&&(d=~~d)}if(c>=p){if(r&&"add"===t.op&&d>l.length)throw new ln("The specified index MUST NOT be greater than the number of elements in the array","OPERATION_VALUE_OUT_OF_BOUNDS",i,t,e);if(!1===(o=un[t.op].call(t,l,d,e)).test)throw new ln("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}}else if(c>=p){if(!1===(o=pn[t.op].call(t,l,d,e)).test)throw new ln("Test operation failed","TEST_OPERATION_FAILED",i,t,e);return o}if(l=l[d],r&&c0)throw new ln('Operation `path` property must start with "/"',"OPERATION_PATH_INVALID",t,e,r);if(("move"===e.op||"copy"===e.op)&&"string"!=typeof e.from)throw new ln("Operation `from` property is not present (applicable in `move` and `copy` operations)","OPERATION_FROM_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&void 0===e.value)throw new ln("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_REQUIRED",t,e,r);if(("add"===e.op||"replace"===e.op||"test"===e.op)&&nn(e.value))throw new ln("Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)","OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED",t,e,r);if(r)if("add"==e.op){var n=e.path.split("/").length,i=s.split("/").length;if(n!==i+1&&n!==i)throw new ln("Cannot perform an `add` operation at the desired path","OPERATION_PATH_CANNOT_ADD",t,e,r)}else if("replace"===e.op||"remove"===e.op||"_get"===e.op){if(e.path!==s)throw new ln("Cannot perform the operation at a path that does not exist","OPERATION_PATH_UNRESOLVABLE",t,e,r)}else if("move"===e.op||"copy"===e.op){var o=gn([{op:"_get",path:e.from,value:void 0}],r);if(o&&"OPERATION_PATH_UNRESOLVABLE"===o.name)throw new ln("Cannot perform the operation from a path that does not exist","OPERATION_FROM_UNRESOLVABLE",t,e,r)}}function gn(e,t,r){try{if(!Array.isArray(e))throw new ln("Patch sequence must be an array","SEQUENCE_NOT_AN_ARRAY");if(t)mn(en(t),en(e),r||!0);else{r=r||fn;for(var s=0;s0&&(e.patches=[],e.callback&&e.callback(s)),s}function Sn(e,t,r,s,n){if(t!==e){"function"==typeof t.toJSON&&(t=t.toJSON());for(var i=Qs(t),o=Qs(e),a=!1,l=o.length-1;l>=0;l--){var c=e[u=o[l]];if(!Zs(t,u)||void 0===t[u]&&void 0!==c&&!1===Array.isArray(t))Array.isArray(e)===Array.isArray(t)?(n&&r.push({op:"test",path:s+"/"+rn(u),value:en(c)}),r.push({op:"remove",path:s+"/"+rn(u)}),a=!0):(n&&r.push({op:"test",path:s,value:e}),r.push({op:"replace",path:s,value:t}));else{var p=t[u];"object"==typeof c&&null!=c&&"object"==typeof p&&null!=p&&Array.isArray(c)===Array.isArray(p)?Sn(c,p,r,s+"/"+rn(u),n):c!==p&&(n&&r.push({op:"test",path:s+"/"+rn(u),value:en(c)}),r.push({op:"replace",path:s+"/"+rn(u),value:en(p)}))}}if(a||i.length!=o.length)for(l=0;lvoid 0!==t&&e?e[t]:e),e)},applyPatch:function(e,t,r){if(r=r||{},"merge"===(t={...t,path:t.path&&Fn(t.path)}).op){const r=Kn(e,t.path);Object.assign(r,t.value),mn(e,[Mn(t.path,r)])}else if("mergeDeep"===t.op){const r=Kn(e,t.path),s=_n(r,t.value);e=mn(e,[Mn(t.path,s)]).newDocument}else if("add"===t.op&&""===t.path&&zn(t.value)){const r=Object.keys(t.value).reduce(((e,r)=>(e.push({op:"add",path:`/${Fn(r)}`,value:t.value[r]}),e)),[]);mn(e,r)}else if("replace"===t.op&&""===t.path){let{value:s}=t;r.allowMetaPatches&&t.meta&&Gn(t)&&(Array.isArray(t.value)||zn(t.value))&&(s={...s,...t.meta}),e=s}else if(mn(e,[t]),r.allowMetaPatches&&t.meta&&Gn(t)&&(Array.isArray(t.value)||zn(t.value))){const r={...Kn(e,t.path),...t.meta};mn(e,[Mn(t.path,r)])}return e},parentPathMatch:function(e,t){if(!Array.isArray(t))return!1;for(let r=0,s=t.length;r(e+"").replace(/~/g,"~0").replace(/\//g,"~1"))).join("/")}`:e}function Mn(e,t,r){return{op:"replace",path:e,value:t,meta:r}}function Ln(e,t,r){return Un(Nn(e.filter(Gn).map((e=>t(e.value,r,e.path)))||[]))}function Dn(e,t,r){return r=r||[],Array.isArray(e)?e.map(((e,s)=>Dn(e,t,r.concat(s)))):zn(e)?Object.keys(e).map((s=>Dn(e[s],t,r.concat(s)))):t(e,r[r.length-1],r)}function Bn(e,t,r){let s=[];if((r=r||[]).length>0){const n=t(e,r[r.length-1],r);n&&(s=s.concat(n))}if(Array.isArray(e)){const n=e.map(((e,s)=>Bn(e,t,r.concat(s))));n&&(s=s.concat(n))}else if(zn(e)){const n=Object.keys(e).map((s=>Bn(e[s],t,r.concat(s))));n&&(s=s.concat(n))}return s=Nn(s),s}function qn(e){return Array.isArray(e)?e:[e]}function Nn(e){return[].concat(...e.map((e=>Array.isArray(e)?Nn(e):e)))}function Un(e){return e.filter((e=>void 0!==e))}function zn(e){return e&&"object"==typeof e}function Hn(e){return e&&"function"==typeof e}function Vn(e){if(Jn(e)){const{op:t}=e;return"add"===t||"remove"===t||"replace"===t}return!1}function Wn(e){return Vn(e)||Jn(e)&&"mutation"===e.type}function Gn(e){return Wn(e)&&("add"===e.op||"replace"===e.op||"merge"===e.op||"mergeDeep"===e.op)}function Jn(e){return e&&"object"==typeof e}function Kn(e,t){try{return dn(e,t)}catch(e){return console.error(e),{}}}var Yn=function(e){return e&&e.Math===Math&&e},Xn=Yn("object"==typeof globalThis&&globalThis)||Yn("object"==typeof window&&window)||Yn("object"==typeof self&&self)||Yn("object"==typeof global&&global)||Yn(!1)||function(){return this}()||Function("return this")(),Zn=function(e){try{return!!e()}catch(e){return!0}},Qn=!Zn((function(){var e=function(){}.bind();return"function"!=typeof e||e.hasOwnProperty("prototype")})),ei=Qn,ti=Function.prototype,ri=ti.apply,si=ti.call,ni="object"==typeof Reflect&&Reflect.apply||(ei?si.bind(ri):function(){return si.apply(ri,arguments)}),ii=Qn,oi=Function.prototype,ai=oi.call,li=ii&&oi.bind.bind(ai,ai),ci=ii?li:function(e){return function(){return ai.apply(e,arguments)}},pi=ci,ui=pi({}.toString),di=pi("".slice),hi=function(e){return di(ui(e),8,-1)},mi=hi,fi=ci,gi=function(e){if("Function"===mi(e))return fi(e)},yi="object"==typeof document&&document.all,vi=void 0===yi&&void 0!==yi?function(e){return"function"==typeof e||e===yi}:function(e){return"function"==typeof e},bi={},xi=!Zn((function(){return 7!==Object.defineProperty({},1,{get:function(){return 7}})[1]})),wi=Qn,$i=Function.prototype.call,Si=wi?$i.bind($i):function(){return $i.apply($i,arguments)},Ei={},ki={}.propertyIsEnumerable,Ai=Object.getOwnPropertyDescriptor,Oi=Ai&&!ki.call({1:2},1);Ei.f=Oi?function(e){var t=Ai(this,e);return!!t&&t.enumerable}:ki;var ji,Ti,Pi=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}},Ci=Zn,Ii=hi,_i=Object,Ri=ci("".split),Fi=Ci((function(){return!_i("z").propertyIsEnumerable(0)}))?function(e){return"String"===Ii(e)?Ri(e,""):_i(e)}:_i,Mi=function(e){return null==e},Li=Mi,Di=TypeError,Bi=function(e){if(Li(e))throw new Di("Can't call method on "+e);return e},qi=Fi,Ni=Bi,Ui=function(e){return qi(Ni(e))},zi=vi,Hi=function(e){return"object"==typeof e?null!==e:zi(e)},Vi={},Wi=Vi,Gi=Xn,Ji=vi,Ki=function(e){return Ji(e)?e:void 0},Yi=function(e,t){return arguments.length<2?Ki(Wi[e])||Ki(Gi[e]):Wi[e]&&Wi[e][t]||Gi[e]&&Gi[e][t]},Xi=ci({}.isPrototypeOf),Zi=Xn.navigator,Qi=Zi&&Zi.userAgent,eo=Xn,to=Qi?String(Qi):"",ro=eo.process,so=eo.Deno,no=ro&&ro.versions||so&&so.version,io=no&&no.v8;io&&(Ti=(ji=io.split("."))[0]>0&&ji[0]<4?1:+(ji[0]+ji[1])),!Ti&&to&&(!(ji=to.match(/Edge\/(\d+)/))||ji[1]>=74)&&(ji=to.match(/Chrome\/(\d+)/))&&(Ti=+ji[1]);var oo=Ti,ao=Zn,lo=Xn.String,co=!!Object.getOwnPropertySymbols&&!ao((function(){var e=Symbol("symbol detection");return!lo(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&oo&&oo<41})),po=co&&!Symbol.sham&&"symbol"==typeof Symbol.iterator,uo=Yi,ho=vi,mo=Xi,fo=Object,go=po?function(e){return"symbol"==typeof e}:function(e){var t=uo("Symbol");return ho(t)&&mo(t.prototype,fo(e))},yo=String,vo=function(e){try{return yo(e)}catch(e){return"Object"}},bo=vi,xo=vo,wo=TypeError,$o=function(e){if(bo(e))return e;throw new wo(xo(e)+" is not a function")},So=$o,Eo=Mi,ko=function(e,t){var r=e[t];return Eo(r)?void 0:So(r)},Ao=Si,Oo=vi,jo=Hi,To=TypeError,Po={exports:{}},Co=Xn,Io=Object.defineProperty,_o=Xn,Ro=Po.exports=_o.o||function(e,t){try{Io(Co,e,{value:t,configurable:!0,writable:!0})}catch(r){Co[e]=t}return t}("__core-js_shared__",{});(Ro.versions||(Ro.versions=[])).push({version:"3.38.1",mode:"pure",copyright:"© 2014-2024 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.38.1/LICENSE",source:"https://github.com/zloirock/core-js"});var Fo=Po.exports,Mo=Fo,Lo=function(e,t){return Mo[e]||(Mo[e]=t||{})},Do=Bi,Bo=Object,qo=function(e){return Bo(Do(e))},No=qo,Uo=ci({}.hasOwnProperty),zo=Object.hasOwn||function(e,t){return Uo(No(e),t)},Ho=ci,Vo=0,Wo=Math.random(),Go=Ho(1..toString),Jo=function(e){return"Symbol("+(void 0===e?"":e)+")_"+Go(++Vo+Wo,36)},Ko=Lo,Yo=zo,Xo=Jo,Zo=co,Qo=po,ea=Xn.Symbol,ta=Ko("wks"),ra=Qo?ea.for||ea:ea&&ea.withoutSetter||Xo,sa=function(e){return Yo(ta,e)||(ta[e]=Zo&&Yo(ea,e)?ea[e]:ra("Symbol."+e)),ta[e]},na=Si,ia=Hi,oa=go,aa=ko,la=TypeError,ca=sa("toPrimitive"),pa=go,ua=function(e){var t=function(e,t){if(!ia(e)||oa(e))return e;var r,s=aa(e,ca);if(s){if(void 0===t&&(t="default"),r=na(s,e,t),!ia(r)||oa(r))return r;throw new la("Can't convert object to primitive value")}return void 0===t&&(t="number"),function(e,t){var r,s;if("string"===t&&Oo(r=e.toString)&&!jo(s=Ao(r,e)))return s;if(Oo(r=e.valueOf)&&!jo(s=Ao(r,e)))return s;if("string"!==t&&Oo(r=e.toString)&&!jo(s=Ao(r,e)))return s;throw new To("Can't convert object to primitive value")}(e,t)}(e,"string");return pa(t)?t:t+""},da=Hi,ha=Xn.document,ma=da(ha)&&da(ha.createElement),fa=function(e){return ma?ha.createElement(e):{}},ga=fa,ya=!xi&&!Zn((function(){return 7!==Object.defineProperty(ga("div"),"a",{get:function(){return 7}}).a})),va=xi,ba=Si,xa=Ei,wa=Pi,$a=Ui,Sa=ua,Ea=zo,ka=ya,Aa=Object.getOwnPropertyDescriptor;bi.f=va?Aa:function(e,t){if(e=$a(e),t=Sa(t),ka)try{return Aa(e,t)}catch(e){}if(Ea(e,t))return wa(!ba(xa.f,e,t),e[t])};var Oa=Zn,ja=vi,Ta=/#|\.prototype\./,Pa=function(e,t){var r=Ia[Ca(e)];return r===Ra||r!==_a&&(ja(t)?Oa(t):!!t)},Ca=Pa.normalize=function(e){return String(e).replace(Ta,".").toLowerCase()},Ia=Pa.data={},_a=Pa.NATIVE="N",Ra=Pa.POLYFILL="P",Fa=Pa,Ma=$o,La=Qn,Da=gi(gi.bind),Ba=function(e,t){return Ma(e),void 0===t?e:La?Da(e,t):function(){return e.apply(t,arguments)}},qa={},Na=xi&&Zn((function(){return 42!==Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype})),Ua=Hi,za=String,Ha=TypeError,Va=function(e){if(Ua(e))return e;throw new Ha(za(e)+" is not an object")},Wa=xi,Ga=ya,Ja=Na,Ka=Va,Ya=ua,Xa=TypeError,Za=Object.defineProperty,Qa=Object.getOwnPropertyDescriptor;qa.f=Wa?Ja?function(e,t,r){if(Ka(e),t=Ya(t),Ka(r),"function"==typeof e&&"prototype"===t&&"value"in r&&"writable"in r&&!r.writable){var s=Qa(e,t);s&&s.writable&&(e[t]=r.value,r={configurable:"configurable"in r?r.configurable:s.configurable,enumerable:"enumerable"in r?r.enumerable:s.enumerable,writable:!1})}return Za(e,t,r)}:Za:function(e,t,r){if(Ka(e),t=Ya(t),Ka(r),Ga)try{return Za(e,t,r)}catch(e){}if("get"in r||"set"in r)throw new Xa("Accessors not supported");return"value"in r&&(e[t]=r.value),e};var el=qa,tl=Pi,rl=xi?function(e,t,r){return el.f(e,t,tl(1,r))}:function(e,t,r){return e[t]=r,e},sl=Xn,nl=ni,il=gi,ol=vi,al=bi.f,ll=Fa,cl=Vi,pl=Ba,ul=rl,dl=zo,hl=function(e){var t=function(r,s,n){if(this instanceof t){switch(arguments.length){case 0:return new e;case 1:return new e(r);case 2:return new e(r,s)}return new e(r,s,n)}return nl(e,this,arguments)};return t.prototype=e.prototype,t},ml=function(e,t){var r,s,n,i,o,a,l,c,p,u=e.target,d=e.global,h=e.stat,m=e.proto,f=d?sl:h?sl[u]:sl[u]&&sl[u].prototype,g=d?cl:cl[u]||ul(cl,u,{})[u],y=g.prototype;for(i in t)s=!(r=ll(d?i:u+(h?".":"#")+i,e.forced))&&f&&dl(f,i),a=g[i],s&&(l=e.dontCallGetSet?(p=al(f,i))&&p.value:f[i]),o=s&&l?l:t[i],(r||m||typeof a!=typeof o)&&(c=e.bind&&s?pl(o,sl):e.wrap&&s?hl(o):m&&ol(o)?il(o):o,(e.sham||o&&o.sham||a&&a.sham)&&ul(c,"sham",!0),ul(g,i,c),m&&(dl(cl,n=u+"Prototype")||ul(cl,n,{}),ul(cl[n],i,o),e.real&&y&&(r||!y[i])&&ul(y,i,o)))},fl=Jo,gl=Lo("keys"),yl=function(e){return gl[e]||(gl[e]=fl(e))},vl=!Zn((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype})),bl=zo,xl=vi,wl=qo,$l=vl,Sl=yl("IE_PROTO"),El=Object,kl=El.prototype,Al=$l?El.getPrototypeOf:function(e){var t=wl(e);if(bl(t,Sl))return t[Sl];var r=t.constructor;return xl(r)&&t instanceof r?r.prototype:t instanceof El?kl:null},Ol=ci,jl=$o,Tl=Hi,Pl=String,Cl=TypeError,Il=Hi,_l=Bi,Rl=Object.setPrototypeOf||("__proto__"in{}?function(){var e,t=!1,r={};try{(e=function(e,t,r){try{return Ol(jl(Object.getOwnPropertyDescriptor(e,"__proto__").set))}catch(e){}}(Object.prototype))(r,[]),t=r instanceof Array}catch(e){}return function(r,s){return _l(r),function(e){if(function(e){return Tl(e)||null===e}(e))return e;throw new Cl("Can't set "+Pl(e)+" as a prototype")}(s),Il(r)?(t?e(r,s):r.__proto__=s,r):r}}():void 0),Fl={},Ml=Math.ceil,Ll=Math.floor,Dl=Math.trunc||function(e){var t=+e;return(t>0?Ll:Ml)(t)},Bl=function(e){var t=+e;return t!=t||0===t?0:Dl(t)},ql=Bl,Nl=Math.max,Ul=Math.min,zl=Bl,Hl=Math.min,Vl=function(e){return function(e){var t=zl(e);return t>0?Hl(t,9007199254740991):0}(e.length)},Wl=Ui,Gl=Vl,Jl=function(e){return function(t,r,s){var n=Wl(t),i=Gl(n);if(0===i)return!e&&-1;var o,a=function(e,t){var r=ql(e);return r<0?Nl(r+t,0):Ul(r,t)}(s,i);if(e&&r!=r){for(;i>a;)if((o=n[a++])!=o)return!0}else for(;i>a;a++)if((e||a in n)&&n[a]===r)return e||a||0;return!e&&-1}},Kl={includes:Jl(!0),indexOf:Jl(!1)},Yl={},Xl=zo,Zl=Ui,Ql=Kl.indexOf,ec=Yl,tc=ci([].push),rc=function(e,t){var r,s=Zl(e),n=0,i=[];for(r in s)!Xl(ec,r)&&Xl(s,r)&&tc(i,r);for(;t.length>n;)Xl(s,r=t[n++])&&(~Ql(i,r)||tc(i,r));return i},sc=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"],nc=rc,ic=sc.concat("length","prototype");Fl.f=Object.getOwnPropertyNames||function(e){return nc(e,ic)};var oc={};oc.f=Object.getOwnPropertySymbols;var ac=Yi,lc=Fl,cc=oc,pc=Va,uc=ci([].concat),dc=ac("Reflect","ownKeys")||function(e){var t=lc.f(pc(e)),r=cc.f;return r?uc(t,r(e)):t},hc=zo,mc=dc,fc=bi,gc=qa,yc=function(e,t,r){for(var s=mc(t),n=gc.f,i=fc.f,o=0;oo;)Ec.f(e,r=n[o++],s[r]);return e};var jc,Tc=Yi("document","documentElement"),Pc=Va,Cc=vc,Ic=sc,_c=Yl,Rc=Tc,Fc=fa,Mc=yl("IE_PROTO"),Lc=function(){},Dc=function(e){return" + + {% block javascript %}{% endblock %}

    Download schema YAML

    + + {% endblock %} \ No newline at end of file diff --git a/promgen/templates/promgen/navbar.html b/promgen/templates/promgen/navbar.html index e701000fb..5b3adc003 100644 --- a/promgen/templates/promgen/navbar.html +++ b/promgen/templates/promgen/navbar.html @@ -33,7 +33,8 @@
  • Import Config
  • -
  • API
  • +
  • API v1
  • +
  • API v2
  • Export Targets
  • Export Rules
  • Export URLs
  • diff --git a/promgen/templates/promgen/profile.html b/promgen/templates/promgen/profile.html index a20657ab2..39da474c3 100644 --- a/promgen/templates/promgen/profile.html +++ b/promgen/templates/promgen/profile.html @@ -7,6 +7,49 @@

    {{user.username}} ({{user.email}})

    +
    +
    User's Token
    + + + + + + + {% if api_token %} + + + {% else %} + + + {% endif %} + + +
    TokenActions
    + {{ api_token }} + +
    + {% csrf_token %} + +
    +
    + {% csrf_token %} + +
    +
    + +
    + {% csrf_token %} + +
    +
    +
    +
    Subscriptions
    diff --git a/promgen/tests/examples/export.targets.json b/promgen/tests/examples/export.targets.json index 12bd5bf53..94d496b80 100644 --- a/promgen/tests/examples/export.targets.json +++ b/promgen/tests/examples/export.targets.json @@ -2,6 +2,7 @@ { "labels": { "__farm_source": "promgen", + "__metrics_path__": "/metrics", "__scheme__": "http", "__shard": "test-shard", "farm": "test-farm", diff --git a/promgen/tests/examples/export.urls.json b/promgen/tests/examples/export.urls.json index 16197b29c..c6ea8db30 100644 --- a/promgen/tests/examples/export.urls.json +++ b/promgen/tests/examples/export.urls.json @@ -1,4 +1,16 @@ [ + { + "labels": { + "__param_module": "http_2xx", + "__shard": "test-shard", + "job": "http_2xx", + "project": "another-project", + "service": "other-service" + }, + "targets": [ + "probe-2.example.com" + ] + }, { "labels": { "__param_module": "fixture_test", @@ -7,6 +19,8 @@ "project": "test-project", "service": "test-service" }, - "targets": ["probe.example.com"] + "targets": [ + "probe.example.com" + ] } ] diff --git a/promgen/tests/examples/rest.audit.default.json b/promgen/tests/examples/rest.audit.default.json new file mode 100644 index 000000000..debae04b1 --- /dev/null +++ b/promgen/tests/examples/rest.audit.default.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "content_type": "project", + "created": "2024-03-19T01:00:00Z", + "log": "Updated test-project", + "new": "", + "object_id": 1, + "old": "", + "user": "demo" + }, + { + "content_type": "service", + "created": "2024-03-19T00:00:00Z", + "log": "Created test-service", + "new": "", + "object_id": 1, + "old": "", + "user": "admin" + } + ] +} diff --git a/promgen/tests/examples/rest.audit.filter_by_content_type.json b/promgen/tests/examples/rest.audit.filter_by_content_type.json new file mode 100644 index 000000000..d058c4454 --- /dev/null +++ b/promgen/tests/examples/rest.audit.filter_by_content_type.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "content_type": "service", + "created": "2024-03-19T00:00:00Z", + "log": "Created test-service", + "new": "", + "object_id": 1, + "old": "", + "user": "admin" + } + ] +} diff --git a/promgen/tests/examples/rest.audit.filter_by_object_id.json b/promgen/tests/examples/rest.audit.filter_by_object_id.json new file mode 100644 index 000000000..debae04b1 --- /dev/null +++ b/promgen/tests/examples/rest.audit.filter_by_object_id.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "content_type": "project", + "created": "2024-03-19T01:00:00Z", + "log": "Updated test-project", + "new": "", + "object_id": 1, + "old": "", + "user": "demo" + }, + { + "content_type": "service", + "created": "2024-03-19T00:00:00Z", + "log": "Created test-service", + "new": "", + "object_id": 1, + "old": "", + "user": "admin" + } + ] +} diff --git a/promgen/tests/examples/rest.audit.filter_by_user.json b/promgen/tests/examples/rest.audit.filter_by_user.json new file mode 100644 index 000000000..a6301d405 --- /dev/null +++ b/promgen/tests/examples/rest.audit.filter_by_user.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "content_type": "project", + "created": "2024-03-19T01:00:00Z", + "log": "Updated test-project", + "new": "", + "object_id": 1, + "old": "", + "user": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.audit.paginated.json b/promgen/tests/examples/rest.audit.paginated.json new file mode 100644 index 000000000..908e49aba --- /dev/null +++ b/promgen/tests/examples/rest.audit.paginated.json @@ -0,0 +1,16 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/logs/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "content_type": "project", + "created": "2024-03-19T01:00:00Z", + "log": "Updated test-project", + "new": "", + "object_id": 1, + "old": "", + "user": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.default.json b/promgen/tests/examples/rest.exporter.default.json new file mode 100644 index 000000000..5d1b8aedf --- /dev/null +++ b/promgen/tests/examples/rest.exporter.default.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "job": "nginx", + "path": "", + "port": 9100, + "project": "another-project", + "scheme": "https" + }, + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.detail.json b/promgen/tests/examples/rest.exporter.detail.json new file mode 100644 index 000000000..ccf8ee7a3 --- /dev/null +++ b/promgen/tests/examples/rest.exporter.detail.json @@ -0,0 +1,9 @@ +{ + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" +} diff --git a/promgen/tests/examples/rest.exporter.filter_by_enabled.json b/promgen/tests/examples/rest.exporter.filter_by_enabled.json new file mode 100644 index 000000000..bd5ed643c --- /dev/null +++ b/promgen/tests/examples/rest.exporter.filter_by_enabled.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.filter_by_job.json b/promgen/tests/examples/rest.exporter.filter_by_job.json new file mode 100644 index 000000000..71ce970aa --- /dev/null +++ b/promgen/tests/examples/rest.exporter.filter_by_job.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "job": "nginx", + "path": "", + "port": 9100, + "project": "another-project", + "scheme": "https" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.filter_by_path.json b/promgen/tests/examples/rest.exporter.filter_by_path.json new file mode 100644 index 000000000..bd5ed643c --- /dev/null +++ b/promgen/tests/examples/rest.exporter.filter_by_path.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.filter_by_project.json b/promgen/tests/examples/rest.exporter.filter_by_project.json new file mode 100644 index 000000000..bd5ed643c --- /dev/null +++ b/promgen/tests/examples/rest.exporter.filter_by_project.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.filter_by_scheme.json b/promgen/tests/examples/rest.exporter.filter_by_scheme.json new file mode 100644 index 000000000..71ce970aa --- /dev/null +++ b/promgen/tests/examples/rest.exporter.filter_by_scheme.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "job": "nginx", + "path": "", + "port": 9100, + "project": "another-project", + "scheme": "https" + } + ] +} diff --git a/promgen/tests/examples/rest.exporter.paginated.json b/promgen/tests/examples/rest.exporter.paginated.json new file mode 100644 index 000000000..9cb1b6992 --- /dev/null +++ b/promgen/tests/examples/rest.exporter.paginated.json @@ -0,0 +1,16 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/exporters/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "job": "nginx", + "path": "", + "port": 9100, + "project": "another-project", + "scheme": "https" + } + ] +} diff --git a/promgen/tests/examples/rest.farm.create.json b/promgen/tests/examples/rest.farm.create.json new file mode 100644 index 000000000..2cca4ab3f --- /dev/null +++ b/promgen/tests/examples/rest.farm.create.json @@ -0,0 +1,6 @@ +{ + "id": 3, + "name": "new-farm", + "source": "promgen", + "owner": "demo" +} diff --git a/promgen/tests/examples/rest.farm.default.json b/promgen/tests/examples/rest.farm.default.json new file mode 100644 index 000000000..2b7e256f4 --- /dev/null +++ b/promgen/tests/examples/rest.farm.default.json @@ -0,0 +1,19 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 2, + "name": "other-farm", + "source": "external", + "owner": null + }, + { + "id": 1, + "name": "test-farm", + "source": "promgen", + "owner": null + } + ] +} diff --git a/promgen/tests/examples/rest.farm.detail.json b/promgen/tests/examples/rest.farm.detail.json new file mode 100644 index 000000000..44bae6dd6 --- /dev/null +++ b/promgen/tests/examples/rest.farm.detail.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "name": "test-farm", + "source": "promgen", + "owner": null +} diff --git a/promgen/tests/examples/rest.farm.filter_by_name.json b/promgen/tests/examples/rest.farm.filter_by_name.json new file mode 100644 index 000000000..1303ab9e3 --- /dev/null +++ b/promgen/tests/examples/rest.farm.filter_by_name.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "test-farm", + "source": "promgen", + "owner": null + } + ] +} diff --git a/promgen/tests/examples/rest.farm.filter_by_source.json b/promgen/tests/examples/rest.farm.filter_by_source.json new file mode 100644 index 000000000..1303ab9e3 --- /dev/null +++ b/promgen/tests/examples/rest.farm.filter_by_source.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "test-farm", + "source": "promgen", + "owner": null + } + ] +} diff --git a/promgen/tests/examples/rest.farm.hosts.json b/promgen/tests/examples/rest.farm.hosts.json new file mode 100644 index 000000000..8d26f73fe --- /dev/null +++ b/promgen/tests/examples/rest.farm.hosts.json @@ -0,0 +1,12 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "farm": 1, + "id": 1, + "name": "example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.farm.paginated.json b/promgen/tests/examples/rest.farm.paginated.json new file mode 100644 index 000000000..80f1112c0 --- /dev/null +++ b/promgen/tests/examples/rest.farm.paginated.json @@ -0,0 +1,13 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/farms/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "id": 2, + "name": "other-farm", + "source": "external", + "owner": null + } + ] +} diff --git a/promgen/tests/examples/rest.farm.partial_update.json b/promgen/tests/examples/rest.farm.partial_update.json new file mode 100644 index 000000000..1d0116bb2 --- /dev/null +++ b/promgen/tests/examples/rest.farm.partial_update.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "name": "new-new-name", + "source": "new-source", + "owner": null +} diff --git a/promgen/tests/examples/rest.farm.projects.json b/promgen/tests/examples/rest.farm.projects.json new file mode 100644 index 000000000..1d0b2fecf --- /dev/null +++ b/promgen/tests/examples/rest.farm.projects.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + }, + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.farm.update.json b/promgen/tests/examples/rest.farm.update.json new file mode 100644 index 000000000..19c348724 --- /dev/null +++ b/promgen/tests/examples/rest.farm.update.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "name": "new-name", + "source": "new-source", + "owner": null +} diff --git a/promgen/tests/examples/rest.farm.1.json b/promgen/tests/examples/rest.farm.v1.detail.json similarity index 60% rename from promgen/tests/examples/rest.farm.1.json rename to promgen/tests/examples/rest.farm.v1.detail.json index 09d9c7cfa..109d534b9 100644 --- a/promgen/tests/examples/rest.farm.1.json +++ b/promgen/tests/examples/rest.farm.v1.detail.json @@ -6,8 +6,8 @@ "owner": null, "hosts": [ { - "name": "host.example.com", - "url": "http://promgen.example.com/host/host.example.com" + "name": "example.com", + "url": "http://promgen.example.com/host/example.com" } ] } diff --git a/promgen/tests/examples/rest.farm.json b/promgen/tests/examples/rest.farm.v1.filter_by_source.json similarity index 82% rename from promgen/tests/examples/rest.farm.json rename to promgen/tests/examples/rest.farm.v1.filter_by_source.json index f6abe8f9a..abe6e7ac7 100644 --- a/promgen/tests/examples/rest.farm.json +++ b/promgen/tests/examples/rest.farm.v1.filter_by_source.json @@ -1,9 +1,9 @@ [ { "id": 1, - "url": "http://promgen.example.com/farm/1", "name": "test-farm", - "source":"promgen", + "source": "promgen", + "url": "http://promgen.example.com/farm/1", "owner": null } ] diff --git a/promgen/tests/examples/rest.farm.v1.json b/promgen/tests/examples/rest.farm.v1.json new file mode 100644 index 000000000..1b439b805 --- /dev/null +++ b/promgen/tests/examples/rest.farm.v1.json @@ -0,0 +1,16 @@ +[ + { + "id": 2, + "name": "other-farm", + "source": "external", + "url": "http://promgen.example.com/farm/2", + "owner": null + }, + { + "id": 1, + "name": "test-farm", + "source": "promgen", + "url": "http://promgen.example.com/farm/1", + "owner": null + } +] diff --git a/promgen/tests/examples/rest.notifier.default.json b/promgen/tests/examples/rest.notifier.default.json new file mode 100644 index 000000000..bb9711ce7 --- /dev/null +++ b/promgen/tests/examples/rest.notifier.default.json @@ -0,0 +1,35 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "id": 1, + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + }, + { + "alias": "", + "content_name": "test-project", + "content_type": "project", + "enabled": true, + "filters": [], + "id": 2, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.filter_by_content_type.json b/promgen/tests/examples/rest.notifier.filter_by_content_type.json new file mode 100644 index 000000000..967eefa31 --- /dev/null +++ b/promgen/tests/examples/rest.notifier.filter_by_content_type.json @@ -0,0 +1,24 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "id": 1, + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.filter_by_object_id.json b/promgen/tests/examples/rest.notifier.filter_by_object_id.json new file mode 100644 index 000000000..bb9711ce7 --- /dev/null +++ b/promgen/tests/examples/rest.notifier.filter_by_object_id.json @@ -0,0 +1,35 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "id": 1, + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + }, + { + "alias": "", + "content_name": "test-project", + "content_type": "project", + "enabled": true, + "filters": [], + "id": 2, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.filter_by_owner.json b/promgen/tests/examples/rest.notifier.filter_by_owner.json new file mode 100644 index 000000000..fa5858f1e --- /dev/null +++ b/promgen/tests/examples/rest.notifier.filter_by_owner.json @@ -0,0 +1,18 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-project", + "content_type": "project", + "enabled": true, + "filters": [], + "id": 2, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.filter_by_sender.json b/promgen/tests/examples/rest.notifier.filter_by_sender.json new file mode 100644 index 000000000..967eefa31 --- /dev/null +++ b/promgen/tests/examples/rest.notifier.filter_by_sender.json @@ -0,0 +1,24 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "id": 1, + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.filter_by_value.json b/promgen/tests/examples/rest.notifier.filter_by_value.json new file mode 100644 index 000000000..fa5858f1e --- /dev/null +++ b/promgen/tests/examples/rest.notifier.filter_by_value.json @@ -0,0 +1,18 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-project", + "content_type": "project", + "enabled": true, + "filters": [], + "id": 2, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + } + ] +} diff --git a/promgen/tests/examples/rest.notifier.paginated.json b/promgen/tests/examples/rest.notifier.paginated.json new file mode 100644 index 000000000..cfefa3844 --- /dev/null +++ b/promgen/tests/examples/rest.notifier.paginated.json @@ -0,0 +1,24 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/notifiers/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "alias": "", + "content_name": "test-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "id": 1, + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.project.create.json b/promgen/tests/examples/rest.project.create.json new file mode 100644 index 000000000..6f1abbaea --- /dev/null +++ b/promgen/tests/examples/rest.project.create.json @@ -0,0 +1,9 @@ +{ + "description": "", + "farm": null, + "id": 3, + "name": "new-project", + "owner": "demo", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.default.json b/promgen/tests/examples/rest.project.default.json new file mode 100644 index 000000000..1d0b2fecf --- /dev/null +++ b/promgen/tests/examples/rest.project.default.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + }, + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.detail.json b/promgen/tests/examples/rest.project.detail.json new file mode 100644 index 000000000..738d33515 --- /dev/null +++ b/promgen/tests/examples/rest.project.detail.json @@ -0,0 +1,9 @@ +{ + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.exporters.json b/promgen/tests/examples/rest.project.exporters.json new file mode 100644 index 000000000..3f986e594 --- /dev/null +++ b/promgen/tests/examples/rest.project.exporters.json @@ -0,0 +1,11 @@ +[ + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "test-project", + "scheme": "http" + } +] diff --git a/promgen/tests/examples/rest.project.filter_by_name.json b/promgen/tests/examples/rest.project.filter_by_name.json new file mode 100644 index 000000000..fcecb90d0 --- /dev/null +++ b/promgen/tests/examples/rest.project.filter_by_name.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.filter_by_owner.json b/promgen/tests/examples/rest.project.filter_by_owner.json new file mode 100644 index 000000000..81147907e --- /dev/null +++ b/promgen/tests/examples/rest.project.filter_by_owner.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.filter_by_service.json b/promgen/tests/examples/rest.project.filter_by_service.json new file mode 100644 index 000000000..fcecb90d0 --- /dev/null +++ b/promgen/tests/examples/rest.project.filter_by_service.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.filter_by_shard.json b/promgen/tests/examples/rest.project.filter_by_shard.json new file mode 100644 index 000000000..1d0b2fecf --- /dev/null +++ b/promgen/tests/examples/rest.project.filter_by_shard.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + }, + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.link_farm.json b/promgen/tests/examples/rest.project.link_farm.json new file mode 100644 index 000000000..ab0abc2d7 --- /dev/null +++ b/promgen/tests/examples/rest.project.link_farm.json @@ -0,0 +1,9 @@ +{ + "description": "Test Project Description", + "farm": "test-farm", + "id": 1, + "name": "updated-project", + "owner": "demo", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.notifiers.json b/promgen/tests/examples/rest.project.notifiers.json new file mode 100644 index 000000000..990766050 --- /dev/null +++ b/promgen/tests/examples/rest.project.notifiers.json @@ -0,0 +1,13 @@ +[ + { + "alias": "", + "content_type": 7, + "enabled": true, + "id": 2, + "label": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX", + "object_id": 1, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + } +] diff --git a/promgen/tests/examples/rest.project.paginated.json b/promgen/tests/examples/rest.project.paginated.json new file mode 100644 index 000000000..c70e3fe23 --- /dev/null +++ b/promgen/tests/examples/rest.project.paginated.json @@ -0,0 +1,16 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/projects/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.project.partial_update.json b/promgen/tests/examples/rest.project.partial_update.json new file mode 100644 index 000000000..ab0abc2d7 --- /dev/null +++ b/promgen/tests/examples/rest.project.partial_update.json @@ -0,0 +1,9 @@ +{ + "description": "Test Project Description", + "farm": "test-farm", + "id": 1, + "name": "updated-project", + "owner": "demo", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.register_exporter.json b/promgen/tests/examples/rest.project.register_exporter.json new file mode 100644 index 000000000..b99873259 --- /dev/null +++ b/promgen/tests/examples/rest.project.register_exporter.json @@ -0,0 +1,20 @@ +[ + { + "enabled": true, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "updated-project", + "scheme": "http" + }, + { + "enabled": true, + "id": 3, + "job": "test-job", + "path": "/metrics", + "port": 8080, + "project": "updated-project", + "scheme": "http" + } +] diff --git a/promgen/tests/examples/rest.project.register_notifier.json b/promgen/tests/examples/rest.project.register_notifier.json new file mode 100644 index 000000000..f36e9391c --- /dev/null +++ b/promgen/tests/examples/rest.project.register_notifier.json @@ -0,0 +1,29 @@ +[ + { + "alias": "", + "content_name": "updated-project", + "content_type": "project", + "enabled": true, + "filters": [], + "id": 2, + "owner": "demo", + "sender": "promgen.notification.slack", + "value": "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" + }, + { + "alias": "", + "content_name": "updated-project", + "content_type": "project", + "enabled": false, + "filters": [ + { + "id": 1, + "name": "test-name", + "value": "test-value" + } + ], + "id": 4, + "sender": "promgen.notification.slack", + "value": "https://test.slack.com" + } +] diff --git a/promgen/tests/examples/rest.project.register_rule.json b/promgen/tests/examples/rest.project.register_rule.json new file mode 100644 index 000000000..2a274e40f --- /dev/null +++ b/promgen/tests/examples/rest.project.register_rule.json @@ -0,0 +1,37 @@ +[ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "updated-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + }, + { + "annotations": { + "rule": "http://promgen.example.com/rule/3", + "summary": "Test Rule Summary" + }, + "clause": "up == 1", + "content_name": "updated-project", + "content_type": "project", + "description": "Test Rule Description", + "duration": "5m", + "enabled": false, + "id": 3, + "labels": { + "severity": "critical" + }, + "name": "TestRuleCreated", + "parent": null + } +] diff --git a/promgen/tests/examples/rest.project.register_url.json b/promgen/tests/examples/rest.project.register_url.json new file mode 100644 index 000000000..e5c4ca5da --- /dev/null +++ b/promgen/tests/examples/rest.project.register_url.json @@ -0,0 +1,14 @@ +[ + { + "id": 3, + "probe": "http_2xx", + "project": "updated-project", + "url": "http://test-url" + }, + { + "id": 1, + "probe": "fixture_test", + "project": "updated-project", + "url": "probe.example.com" + } +] diff --git a/promgen/tests/examples/rest.project.rules.json b/promgen/tests/examples/rest.project.rules.json new file mode 100644 index 000000000..c38184da9 --- /dev/null +++ b/promgen/tests/examples/rest.project.rules.json @@ -0,0 +1,19 @@ +[ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + } +] diff --git a/promgen/tests/examples/rest.project.unlink_farm.json b/promgen/tests/examples/rest.project.unlink_farm.json new file mode 100644 index 000000000..65d042af3 --- /dev/null +++ b/promgen/tests/examples/rest.project.unlink_farm.json @@ -0,0 +1,9 @@ +{ + "description": "Test Project Description", + "farm": null, + "id": 1, + "name": "updated-project", + "owner": "demo", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.update.json b/promgen/tests/examples/rest.project.update.json new file mode 100644 index 000000000..ab0abc2d7 --- /dev/null +++ b/promgen/tests/examples/rest.project.update.json @@ -0,0 +1,9 @@ +{ + "description": "Test Project Description", + "farm": "test-farm", + "id": 1, + "name": "updated-project", + "owner": "demo", + "service": "test-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.project.update_exporter.json b/promgen/tests/examples/rest.project.update_exporter.json new file mode 100644 index 000000000..f558628b5 --- /dev/null +++ b/promgen/tests/examples/rest.project.update_exporter.json @@ -0,0 +1,20 @@ +[ + { + "enabled": false, + "id": 1, + "job": "node", + "path": "/metrics", + "port": 9100, + "project": "updated-project", + "scheme": "http" + }, + { + "enabled": true, + "id": 3, + "job": "test-job", + "path": "/metrics", + "port": 8080, + "project": "updated-project", + "scheme": "http" + } +] diff --git a/promgen/tests/examples/rest.project.urls.json b/promgen/tests/examples/rest.project.urls.json new file mode 100644 index 000000000..198ebaf68 --- /dev/null +++ b/promgen/tests/examples/rest.project.urls.json @@ -0,0 +1,8 @@ +[ + { + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" + } +] diff --git a/promgen/tests/examples/rest.rule.default.json b/promgen/tests/examples/rest.rule.default.json new file mode 100644 index 000000000..ee5d08051 --- /dev/null +++ b/promgen/tests/examples/rest.rule.default.json @@ -0,0 +1,41 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + }, + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + } + ] +} diff --git a/promgen/tests/examples/rest.rule.detail.json b/promgen/tests/examples/rest.rule.detail.json new file mode 100644 index 000000000..abda812a0 --- /dev/null +++ b/promgen/tests/examples/rest.rule.detail.json @@ -0,0 +1,17 @@ +{ + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null +} diff --git a/promgen/tests/examples/rest.rule.filter_by_content_type.json b/promgen/tests/examples/rest.rule.filter_by_content_type.json new file mode 100644 index 000000000..97eee611c --- /dev/null +++ b/promgen/tests/examples/rest.rule.filter_by_content_type.json @@ -0,0 +1,24 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + } + ] +} diff --git a/promgen/tests/examples/rest.rule.filter_by_enabled.json b/promgen/tests/examples/rest.rule.filter_by_enabled.json new file mode 100644 index 000000000..97eee611c --- /dev/null +++ b/promgen/tests/examples/rest.rule.filter_by_enabled.json @@ -0,0 +1,24 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + } + ] +} diff --git a/promgen/tests/examples/rest.rule.filter_by_object_id.json b/promgen/tests/examples/rest.rule.filter_by_object_id.json new file mode 100644 index 000000000..ee5d08051 --- /dev/null +++ b/promgen/tests/examples/rest.rule.filter_by_object_id.json @@ -0,0 +1,41 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + }, + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + } + ] +} diff --git a/promgen/tests/examples/rest.rule.filter_by_parent.json b/promgen/tests/examples/rest.rule.filter_by_parent.json new file mode 100644 index 000000000..c95a6644f --- /dev/null +++ b/promgen/tests/examples/rest.rule.filter_by_parent.json @@ -0,0 +1,24 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + } + ] +} diff --git a/promgen/tests/examples/rest.rule.paginated.json b/promgen/tests/examples/rest.rule.paginated.json new file mode 100644 index 000000000..685a2d1fe --- /dev/null +++ b/promgen/tests/examples/rest.rule.paginated.json @@ -0,0 +1,24 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/rules/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": false, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "Test Rule" + } + ] +} diff --git a/promgen/tests/examples/rest.rule.partial_update.json b/promgen/tests/examples/rest.rule.partial_update.json new file mode 100644 index 000000000..dd556ebb7 --- /dev/null +++ b/promgen/tests/examples/rest.rule.partial_update.json @@ -0,0 +1,17 @@ +{ + "annotations": { + "rule": "http://promgen.example.com/rule/2" + }, + "clause": "up != 0", + "content_name": "test-project", + "content_type": "project", + "description": "Test Rule 2 Description", + "duration": "1m", + "enabled": true, + "id": 2, + "labels": { + "severity": "warning" + }, + "name": "Test Rule 2", + "parent": "TestRuleUpdated" +} diff --git a/promgen/tests/examples/rest.rule.update.json b/promgen/tests/examples/rest.rule.update.json new file mode 100644 index 000000000..ff2c748f2 --- /dev/null +++ b/promgen/tests/examples/rest.rule.update.json @@ -0,0 +1,18 @@ +{ + "annotations": { + "rule": "http://promgen.example.com/rule/1", + "summary": "Test Rule Summary" + }, + "clause": "up == 1", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": false, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "TestRuleUpdated", + "parent": null +} diff --git a/promgen/tests/examples/rest.service.create.json b/promgen/tests/examples/rest.service.create.json new file mode 100644 index 000000000..c58281c97 --- /dev/null +++ b/promgen/tests/examples/rest.service.create.json @@ -0,0 +1,6 @@ +{ + "description": "Test New Service Description", + "id": 3, + "name": "new-service", + "owner": "demo" +} diff --git a/promgen/tests/examples/rest.service.default.json b/promgen/tests/examples/rest.service.default.json new file mode 100644 index 000000000..4333a77ec --- /dev/null +++ b/promgen/tests/examples/rest.service.default.json @@ -0,0 +1,19 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "id": 2, + "name": "other-service", + "owner": "demo" + }, + { + "description": "", + "id": 1, + "name": "test-service", + "owner": "admin" + } + ] +} diff --git a/promgen/tests/examples/rest.service.detail.json b/promgen/tests/examples/rest.service.detail.json new file mode 100644 index 000000000..118c40605 --- /dev/null +++ b/promgen/tests/examples/rest.service.detail.json @@ -0,0 +1,6 @@ +{ + "description": "", + "id": 1, + "name": "test-service", + "owner": "admin" +} diff --git a/promgen/tests/examples/rest.service.filter_by_name.json b/promgen/tests/examples/rest.service.filter_by_name.json new file mode 100644 index 000000000..e6373c316 --- /dev/null +++ b/promgen/tests/examples/rest.service.filter_by_name.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "id": 1, + "name": "test-service", + "owner": "admin" + } + ] +} diff --git a/promgen/tests/examples/rest.service.filter_by_owner.json b/promgen/tests/examples/rest.service.filter_by_owner.json new file mode 100644 index 000000000..45813b7ed --- /dev/null +++ b/promgen/tests/examples/rest.service.filter_by_owner.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "id": 2, + "name": "other-service", + "owner": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.service.notifiers.json b/promgen/tests/examples/rest.service.notifiers.json new file mode 100644 index 000000000..12313d5c3 --- /dev/null +++ b/promgen/tests/examples/rest.service.notifiers.json @@ -0,0 +1,13 @@ +[ + { + "alias": "", + "content_type": 11, + "enabled": true, + "id": 1, + "label": "email@example.com", + "object_id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + } +] diff --git a/promgen/tests/examples/rest.service.paginated.json b/promgen/tests/examples/rest.service.paginated.json new file mode 100644 index 000000000..11ddb4c8f --- /dev/null +++ b/promgen/tests/examples/rest.service.paginated.json @@ -0,0 +1,13 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/services/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "description": "", + "id": 2, + "name": "other-service", + "owner": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.service.partial_update.json b/promgen/tests/examples/rest.service.partial_update.json new file mode 100644 index 000000000..bbb26e6dd --- /dev/null +++ b/promgen/tests/examples/rest.service.partial_update.json @@ -0,0 +1,6 @@ +{ + "description": "Test Updated Description", + "id": 1, + "name": "partial-updated-service", + "owner": "demo" +} diff --git a/promgen/tests/examples/rest.service.projects.json b/promgen/tests/examples/rest.service.projects.json new file mode 100644 index 000000000..8311f3c2a --- /dev/null +++ b/promgen/tests/examples/rest.service.projects.json @@ -0,0 +1,11 @@ +[ + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } +] \ No newline at end of file diff --git a/promgen/tests/examples/rest.service.register_notifier.json b/promgen/tests/examples/rest.service.register_notifier.json new file mode 100644 index 000000000..184efa59b --- /dev/null +++ b/promgen/tests/examples/rest.service.register_notifier.json @@ -0,0 +1,33 @@ +[ + { + "alias": "", + "content_name": "partial-updated-service", + "content_type": "service", + "enabled": true, + "filters": [ + { + "name": "severity", + "value": "critical" + } + ], + "id": 1, + "owner": "admin", + "sender": "promgen.notification.email", + "value": "email@example.com" + }, + { + "alias": "", + "content_name": "partial-updated-service", + "content_type": "service", + "enabled": false, + "filters": [ + { + "name": "test-name", + "value": "test-value" + } + ], + "id": 6, + "sender": "promgen.notification.slack", + "value": "https://test.slack.com" + } +] diff --git a/promgen/tests/examples/rest.service.register_project.json b/promgen/tests/examples/rest.service.register_project.json new file mode 100644 index 000000000..b20738d94 --- /dev/null +++ b/promgen/tests/examples/rest.service.register_project.json @@ -0,0 +1,9 @@ +{ + "description": "", + "farm": null, + "id": 3, + "name": "new-project", + "owner": "demo", + "service": "partial-updated-service", + "shard": "test-shard" +} diff --git a/promgen/tests/examples/rest.service.register_rule.json b/promgen/tests/examples/rest.service.register_rule.json new file mode 100644 index 000000000..1263d160c --- /dev/null +++ b/promgen/tests/examples/rest.service.register_rule.json @@ -0,0 +1,37 @@ +[ + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "partial-updated-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + }, + { + "annotations": { + "rule": "http://promgen.example.com/rule/4", + "summary": "Test Rule Summary" + }, + "clause": "up == 1", + "content_name": "partial-updated-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": false, + "id": 4, + "labels": { + "severity": "critical" + }, + "name": "TestRuleCreated", + "parent": null + } +] diff --git a/promgen/tests/examples/rest.service.rules.json b/promgen/tests/examples/rest.service.rules.json new file mode 100644 index 000000000..5a092e186 --- /dev/null +++ b/promgen/tests/examples/rest.service.rules.json @@ -0,0 +1,19 @@ +[ + { + "annotations": { + "rule": "http://promgen.example.com/rule/1" + }, + "clause": "up == 0", + "content_name": "test-service", + "content_type": "service", + "description": "Test Rule Description", + "duration": "5m", + "enabled": true, + "id": 1, + "labels": { + "severity": "critical" + }, + "name": "Test Rule", + "parent": null + } +] diff --git a/promgen/tests/examples/rest.service.update.json b/promgen/tests/examples/rest.service.update.json new file mode 100644 index 000000000..5bc7dee8f --- /dev/null +++ b/promgen/tests/examples/rest.service.update.json @@ -0,0 +1,6 @@ +{ + "description": "Test Updated Description", + "id": 1, + "name": "updated-service", + "owner": "demo" +} diff --git a/promgen/tests/examples/rest.shard.default.json b/promgen/tests/examples/rest.shard.default.json new file mode 100644 index 000000000..2ffe4030b --- /dev/null +++ b/promgen/tests/examples/rest.shard.default.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "name": "other-shard", + "proxy": false, + "samples": 5000000, + "targets": 10000, + "url": "http://prometheus-002.example.com" + }, + { + "enabled": true, + "id": 1, + "name": "test-shard", + "proxy": true, + "samples": 5000000, + "targets": 10000, + "url": "http://prometheus.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.shard.detail.json b/promgen/tests/examples/rest.shard.detail.json new file mode 100644 index 000000000..b23d9fe99 --- /dev/null +++ b/promgen/tests/examples/rest.shard.detail.json @@ -0,0 +1,9 @@ +{ + "enabled": true, + "id": 1, + "name": "test-shard", + "proxy": true, + "samples": 5000000, + "targets": 10000, + "url": "http://prometheus.example.com" +} diff --git a/promgen/tests/examples/rest.shard.filter_by_name.json b/promgen/tests/examples/rest.shard.filter_by_name.json new file mode 100644 index 000000000..28c213713 --- /dev/null +++ b/promgen/tests/examples/rest.shard.filter_by_name.json @@ -0,0 +1,16 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "enabled": true, + "id": 1, + "name": "test-shard", + "proxy": true, + "samples": 5000000, + "targets": 10000, + "url": "http://prometheus.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.shard.paginated.json b/promgen/tests/examples/rest.shard.paginated.json new file mode 100644 index 000000000..be18becaa --- /dev/null +++ b/promgen/tests/examples/rest.shard.paginated.json @@ -0,0 +1,16 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/shards/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "enabled": false, + "id": 2, + "name": "other-shard", + "proxy": false, + "samples": 5000000, + "targets": 10000, + "url": "http://prometheus-002.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.shard.projects.json b/promgen/tests/examples/rest.shard.projects.json new file mode 100644 index 000000000..1d0b2fecf --- /dev/null +++ b/promgen/tests/examples/rest.shard.projects.json @@ -0,0 +1,25 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "description": "", + "farm": "test-farm", + "id": 2, + "name": "another-project", + "owner": "demo", + "service": "other-service", + "shard": "test-shard" + }, + { + "description": "", + "farm": "test-farm", + "id": 1, + "name": "test-project", + "owner": "admin", + "service": "test-service", + "shard": "test-shard" + } + ] +} diff --git a/promgen/tests/examples/rest.url.default.json b/promgen/tests/examples/rest.url.default.json new file mode 100644 index 000000000..1f0837b0b --- /dev/null +++ b/promgen/tests/examples/rest.url.default.json @@ -0,0 +1,19 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 2, + "probe": "http_2xx", + "project": "another-project", + "url": "probe-2.example.com" + }, + { + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.url.detail.json b/promgen/tests/examples/rest.url.detail.json new file mode 100644 index 000000000..6bee9a6b1 --- /dev/null +++ b/promgen/tests/examples/rest.url.detail.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" +} diff --git a/promgen/tests/examples/rest.url.filter_by_probe.json b/promgen/tests/examples/rest.url.filter_by_probe.json new file mode 100644 index 000000000..d52936a7a --- /dev/null +++ b/promgen/tests/examples/rest.url.filter_by_probe.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.url.filter_by_project.json b/promgen/tests/examples/rest.url.filter_by_project.json new file mode 100644 index 000000000..d52936a7a --- /dev/null +++ b/promgen/tests/examples/rest.url.filter_by_project.json @@ -0,0 +1,13 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.url.paginated.json b/promgen/tests/examples/rest.url.paginated.json new file mode 100644 index 000000000..b545071dd --- /dev/null +++ b/promgen/tests/examples/rest.url.paginated.json @@ -0,0 +1,13 @@ +{ + "count": 2, + "next": null, + "previous": "http://testserver/rest/v2/urls/?page_size=1", + "results": [ + { + "id": 1, + "probe": "fixture_test", + "project": "test-project", + "url": "probe.example.com" + } + ] +} diff --git a/promgen/tests/examples/rest.user.default.json b/promgen/tests/examples/rest.user.default.json new file mode 100644 index 000000000..56178bf6c --- /dev/null +++ b/promgen/tests/examples/rest.user.default.json @@ -0,0 +1,21 @@ +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "first_name": "", + "last_name": "" + }, + { + "id": 2, + "username": "demo", + "email": "demo@example.com", + "first_name": "", + "last_name": "" + } + ] +} diff --git a/promgen/tests/examples/rest.user.filter_by_email.json b/promgen/tests/examples/rest.user.filter_by_email.json new file mode 100644 index 000000000..1ccdf45fe --- /dev/null +++ b/promgen/tests/examples/rest.user.filter_by_email.json @@ -0,0 +1,14 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "email": "demo@example.com", + "first_name": "", + "id": 2, + "last_name": "", + "username": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.user.filter_by_username.json b/promgen/tests/examples/rest.user.filter_by_username.json new file mode 100644 index 000000000..1ccdf45fe --- /dev/null +++ b/promgen/tests/examples/rest.user.filter_by_username.json @@ -0,0 +1,14 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "email": "demo@example.com", + "first_name": "", + "id": 2, + "last_name": "", + "username": "demo" + } + ] +} diff --git a/promgen/tests/examples/rest.user.get_current_user.json b/promgen/tests/examples/rest.user.get_current_user.json new file mode 100644 index 000000000..48aa9a3a9 --- /dev/null +++ b/promgen/tests/examples/rest.user.get_current_user.json @@ -0,0 +1,11 @@ +{ + "id": 2, + "username": "demo", + "email": "demo@example.com", + "first_name": "", + "last_name": "", + "is_staff": false, + "is_superuser": false, + "date_joined": null, + "last_login": null +} diff --git a/promgen/tests/examples/rest.user.paginated.json b/promgen/tests/examples/rest.user.paginated.json new file mode 100644 index 000000000..5fc524d70 --- /dev/null +++ b/promgen/tests/examples/rest.user.paginated.json @@ -0,0 +1,14 @@ +{ + "count": 2, + "next": "http://testserver/rest/v2/users/?page_number=2&page_size=1", + "previous": null, + "results": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "first_name": "", + "last_name": "" + } + ] +} diff --git a/promgen/tests/notification/test_email.py b/promgen/tests/notification/test_email.py index b97cd765d..24660100f 100644 --- a/promgen/tests/notification/test_email.py +++ b/promgen/tests/notification/test_email.py @@ -14,6 +14,10 @@ def setUp(self): one = models.Project.objects.get(pk=1) two = models.Project.objects.get(pk=2) + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + NotificationEmail.create(obj=one, value="example@example.com") NotificationEmail.create(obj=one, value="foo@example.com") NotificationEmail.create(obj=two, value="bar@example.com") diff --git a/promgen/tests/notification/test_linenotify.py b/promgen/tests/notification/test_linenotify.py index 3076c6120..66d1b313a 100644 --- a/promgen/tests/notification/test_linenotify.py +++ b/promgen/tests/notification/test_linenotify.py @@ -14,6 +14,10 @@ def setUp(self): one = models.Project.objects.get(pk=1) two = models.Service.objects.get(pk=2) + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + NotificationLineNotify.create(obj=one, value="hogehoge") NotificationLineNotify.create(obj=two, value="asdfasdf") diff --git a/promgen/tests/notification/test_slack.py b/promgen/tests/notification/test_slack.py index da2c36a1c..f22cf2a22 100644 --- a/promgen/tests/notification/test_slack.py +++ b/promgen/tests/notification/test_slack.py @@ -17,6 +17,10 @@ def setUp(self): one = models.Project.objects.get(pk=1) two = models.Service.objects.get(pk=2) + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + NotificationSlack.create(obj=one, value=self.TestHook1) NotificationSlack.create(obj=two, value=self.TestHook2) diff --git a/promgen/tests/notification/test_user_email.py b/promgen/tests/notification/test_user_email.py index e2cef0cc8..d3cdf143a 100644 --- a/promgen/tests/notification/test_user_email.py +++ b/promgen/tests/notification/test_user_email.py @@ -19,6 +19,8 @@ class UserSplayTest(tests.PromgenTest): def test_user_splay(self, mock_email, mock_post): one = models.Service.objects.get(pk=1) + models.Sender.objects.all().delete() + NotificationUser.create(obj=one, value=one.owner.username) NotificationLineNotify.create(obj=one.owner, value="#foo") NotificationEmail.create(obj=one.owner, value="foo@bar.example") @@ -41,6 +43,11 @@ def test_failed_user(self, mock_email): # The invalid one should be skipped while still letting # the valid one pass one = models.Service.objects.get(pk=1) + + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + NotificationEmail.create(obj=one, value="foo@bar.example") NotificationUser.create(obj=one, value="does not exist") @@ -57,6 +64,10 @@ def test_failed_user(self, mock_email): def test_enabled(self, mock_email): one = models.Service.objects.get(pk=1) + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + # This notification is direct and disabled NotificationEmail.create(obj=one, value="disabled.example@example.com", enabled=False) # Our parent notification is enabled diff --git a/promgen/tests/notification/test_webhook.py b/promgen/tests/notification/test_webhook.py index 091fc03b6..8093a05b1 100644 --- a/promgen/tests/notification/test_webhook.py +++ b/promgen/tests/notification/test_webhook.py @@ -15,6 +15,10 @@ def setUp(self): one = models.Project.objects.get(pk=1) two = models.Service.objects.get(pk=1) + # Firstly, clear all Sender data in test database to ensure avoiding data conflicts + # without having to make too many changes in old tests. + models.Sender.objects.all().delete() + self.senderA = NotificationWebhook.create( obj=one, value="http://webhook.example.com/project" ) diff --git a/promgen/tests/test_alert_rules.py b/promgen/tests/test_alert_rules.py index a448a92e5..355da472a 100644 --- a/promgen/tests/test_alert_rules.py +++ b/promgen/tests/test_alert_rules.py @@ -57,7 +57,7 @@ def test_import_v2(self, mock_post): # Includes count of our setUp rule + imported rules self.assertRoute(response, views.RuleImport, status=200) - self.assertCount(models.Rule, 3, "Missing Rule") + self.assertCount(models.Rule, 4, "Missing Rule") @skip @override_settings(PROMGEN=TEST_SETTINGS) @@ -98,8 +98,8 @@ def test_missing_permission(self, mock_post): {"rules": tests.Data("examples", "import.rule.yml").raw()}, ) - # Should only be a single rule from our initial setup - self.assertCount(models.Rule, 1, "Missing Rule") + # Should only be number of rules from our initial setup + self.assertCount(models.Rule, 2, "Missing Rule") @mock.patch("django.dispatch.dispatcher.Signal.send") def test_macro(self, mock_post): diff --git a/promgen/tests/test_host_add.py b/promgen/tests/test_host_add.py index 41c52d0e0..f7bbbda2a 100644 --- a/promgen/tests/test_host_add.py +++ b/promgen/tests/test_host_add.py @@ -21,7 +21,7 @@ def test_newline(self): {"hosts": "\naaa.example.com\nbbb.example.com\nccc.example.com \n"}, follow=False, ) - self.assertCount(models.Host, 3, "Expected 3 hosts") + self.assertCount(models.Host, 4, "Expected 4 hosts (Fixture has one host)") def test_comma(self): self.client.post( @@ -29,7 +29,7 @@ def test_comma(self): {"hosts": ",,aaa.example.com, bbb.example.com,ccc.example.com,"}, follow=False, ) - self.assertCount(models.Host, 3, "Expected 3 hosts") + self.assertCount(models.Host, 4, "Expected 4 hosts (Fixture has one host)") # Within our new host code, the hosts are split (by newline or comma) and then # individually tested. Here we will test our validator on specific entries that diff --git a/promgen/tests/test_rest.py b/promgen/tests/test_rest.py index 04c77cf4c..10677c1a9 100644 --- a/promgen/tests/test_rest.py +++ b/promgen/tests/test_rest.py @@ -1,14 +1,20 @@ # Copyright (c) 2018 LINE Corporation # These sources are released under the terms of the MIT license: see LICENSE - - +import django.core.cache +from django.contrib.auth.models import User, Permission from django.test import override_settings from django.urls import reverse +from rest_framework.authtoken.models import Token from promgen import models, rest, tests class RestAPITest(tests.PromgenTest): + def setUp(self): + super().setUp() + # Clear the cache before each test to reset throttling + django.core.cache.cache.clear() + @override_settings(PROMGEN=tests.SETTINGS) @override_settings(CELERY_TASK_ALWAYS_EAGER=True) def test_alert_blackhole(self): @@ -24,39 +30,2291 @@ def test_alert(self): self.assertCount(models.Alert, 1, "Alert Queued") @override_settings(PROMGEN=tests.SETTINGS) - def test_retrieve_farm(self): - expected = tests.Data("examples", "rest.farm.json").json() + def test_rest_farm(self): + token = Token.objects.filter(user__username="demo").first().key - # Check retrieving all farms + # Test V1 API + expected = tests.Data("examples", "rest.farm.v1.json").json() + + # Check retrieving farms without token returns 401 Unauthorized response = self.client.get(reverse("api:farm-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all farms with token successfully + response = self.client.get( + reverse("api:farm-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), expected) # Check retrieving all farms whose "name" contains "farm" - response = self.client.get(reverse("api:farm-list"), {"name": "farm"}) + response = self.client.get( + reverse("api:farm-list"), + {"name": "farm"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), expected) # Check retrieving all farms whose "source" is "promgen" - response = self.client.get(reverse("api:farm-list"), {"source": "promgen"}) + expected = tests.Data("examples", "rest.farm.v1.filter_by_source.json").json() + response = self.client.get( + reverse("api:farm-list"), + {"source": "promgen"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), expected) # Check retrieving farms with a non-existent "name" returns an empty list - response = self.client.get(reverse("api:farm-list"), {"name": "other-name"}) + response = self.client.get( + reverse("api:farm-list"), + {"name": "other-name"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 0) - # Check retrieving farms with a non-existent "source" returns an empty list - response = self.client.get(reverse("api:farm-list"), {"source": "other-source"}) + # Check retrieving farms with a non-existent "source" returns 400 Bad Request + response = self.client.get( + reverse("api:farm-list"), + {"source": "other-source"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving the farm whose "id" is "1" without token returns 401 Unauthorized + expected = tests.Data("examples", "rest.farm.v1.detail.json").json() + response = self.client.get( + reverse("api:farm-detail", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check retrieving the farm whose "id" is "1", including the list of hosts. + expected = tests.Data("examples", "rest.farm.v1.detail.json").json() + response = self.client.get( + reverse("api:farm-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 0) + self.assertEqual(response.json(), expected) + # Test V2 API + # Check retrieving all farms + expected = tests.Data("examples", "rest.farm.default.json").json() + response = self.client.get(reverse("api-v2:farm-list")) + self.assertEqual(response.status_code, 401) - farm = models.Farm.objects.get(id=1) - models.Host.objects.create(name="host.example.com", farm=farm) - expected = tests.Data("examples", "rest.farm.1.json").json() + # Check retrieving all farms + expected = tests.Data("examples", "rest.farm.default.json").json() + response = self.client.get( + reverse("api-v2:farm-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) - # Check retrieving the farm whose "id" is "1", including the list of hosts. - response = self.client.get(reverse("api:farm-detail", args=[1])) + # Check retrieving paginated farms + expected = tests.Data("examples", "rest.farm.paginated.json").json() + response = self.client.get( + reverse("api-v2:farm-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving farms whose "name" contains "farm" + expected = tests.Data("examples", "rest.farm.filter_by_name.json").json() + response = self.client.get( + reverse("api-v2:farm-list"), + {"name": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving farms with a non-existent "name" returns an empty list + response = self.client.get( + reverse("api-v2:farm-list"), + {"name": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving farms whose "source" is "promgen" + expected = tests.Data("examples", "rest.farm.filter_by_source.json").json() + response = self.client.get( + reverse("api-v2:farm-list"), + {"source": "promgen"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving farms with a non-existent "source" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:farm-list"), + {"source": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving the farm whose "id" is "1" + expected = tests.Data("examples", "rest.farm.detail.json").json() + response = self.client.get( + reverse("api-v2:farm-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving the list of hosts of the farm whose "id" is "1" + expected = tests.Data("examples", "rest.farm.hosts.json").json() + response = self.client.get( + reverse("api-v2:farm-hosts", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving the list of hosts of the farm + # with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:farm-hosts", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check retrieving the list of projects of the farm whose "id" is "1" + expected = tests.Data("examples", "rest.farm.projects.json").json() + response = self.client.get( + reverse("api-v2:farm-projects", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving the list of projects of the farm + # with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:farm-projects", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check create a farm without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:farm-list"), + {"name": "new-farm", "source": "promgen"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check create a farm without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:farm-list"), + {"name": "new-farm", "source": "promgen"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check update a farm without token returns 401 Unauthorized + response = self.client.put( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-name", "source": "promgen"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check update a farm without permission returns 403 Forbidden + response = self.client.put( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-name", "source": "promgen"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check partial update a farm without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-new-name"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check partial update a farm without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-new-name"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register hosts for a farm without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:farm-hosts", args=[1]), + {"hosts": ["new-host", "new-host-2"]}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register hosts for a farm without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:farm-hosts", args=[1]), + {"hosts": ["new-host", "new-host-2"]}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete hosts for a farm without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:farm-delete-host", args=[1, 2]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete hosts for a farm without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:farm-delete-host", args=[1, 2]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete a farm without token returns 401 Unauthorized + response = self.client.delete(reverse("api-v2:farm-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check delete a farm without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:farm-detail", args=[1]), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 403) + + user = User.objects.get(username="demo") + user.user_permissions.add(Permission.objects.get(codename="add_farm")) + user.user_permissions.add(Permission.objects.get(codename="change_farm")) + user.user_permissions.add(Permission.objects.get(codename="delete_farm")) + + # Check create a farm successfully with permission + expected = tests.Data("examples", "rest.farm.create.json").json() + before_count = models.Farm.objects.count() + response = self.client.post( + reverse("api-v2:farm-list"), + {"name": "new-farm", "source": "promgen"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + after_count = models.Farm.objects.count() + self.assertEqual(response.status_code, 201) + self.assertEqual(before_count + 1, after_count) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + expected.pop("id", None) + response.json().pop("id", None) + self.assertEqual(response.json(), expected) + + # Check update a farm successfully with permission + expected = tests.Data("examples", "rest.farm.update.json").json() + response = self.client.put( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-name", "source": "new-source"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check partial update a farm successfully with permission + expected = tests.Data("examples", "rest.farm.partial_update.json").json() + response = self.client.patch( + reverse("api-v2:farm-detail", args=[1]), + {"name": "new-new-name"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check register hosts for a farm successfully with permission + response = self.client.post( + reverse("api-v2:farm-hosts", args=[1]), + {"hosts": ["new-host", "new-host-2"]}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + + # Check delete hosts for a farm successfully with permission + response = self.client.delete( + reverse("api-v2:farm-delete-host", args=[1, 2]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + # Check delete a farm successfully with permission + response = self.client.delete( + reverse("api-v2:farm-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_user(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving all users without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:user-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all users + expected = tests.Data("examples", "rest.user.default.json").json() + response = self.client.get(reverse("api-v2:user-list"), HTTP_AUTHORIZATION=f"Token {token}") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated users + expected = tests.Data("examples", "rest.user.paginated.json").json() + response = self.client.get( + reverse("api-v2:user-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving users whose "username" contains "demo" + expected = tests.Data("examples", "rest.user.filter_by_username.json").json() + response = self.client.get( + reverse("api-v2:user-list"), {"username": "demo"}, HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving users with a non-existent "username" returns an empty list + response = self.client.get( + reverse("api-v2:user-list"), + {"username": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving users whose "email" contains "demo@example" + expected = tests.Data("examples", "rest.user.filter_by_email.json").json() + response = self.client.get( + reverse("api-v2:user-list"), + {"email": "demo@example"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving users with a non-existent "email" returns an empty list + response = self.client.get( + reverse("api-v2:user-list"), + {"email": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving the current user without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:user-get-current-user")) + self.assertEqual(response.status_code, 401) + + # Check retrieving the current user with token returns the expected user + expected = tests.Data("examples", "rest.user.get_current_user.json").json() + response = self.client.get( + reverse("api-v2:user-get-current-user"), HTTP_AUTHORIZATION=f"Token {token}" + ) + response_json = response.json() + # date_joined is not a fixed value when running tests, so we need to skip it + response_json["date_joined"] = None + self.assertEqual(response.status_code, 200) + self.assertEqual(response_json, expected) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_audit(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving all audits without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:audit-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all audits + expected = tests.Data("examples", "rest.audit.default.json").json() + response = self.client.get( + reverse("api-v2:audit-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated audits + expected = tests.Data("examples", "rest.audit.paginated.json").json() + response = self.client.get( + reverse("api-v2:audit-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving audits whose "content_type" is "service" + expected = tests.Data("examples", "rest.audit.filter_by_content_type.json").json() + response = self.client.get( + reverse("api-v2:audit-list"), + {"content_type": "service"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving audits with a non-allowed "content_type" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:audit-list"), + {"content_type": "non-allowed"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving audits whose "object_id" is "1" + expected = tests.Data("examples", "rest.audit.filter_by_object_id.json").json() + response = self.client.get( + reverse("api-v2:audit-list"), + {"object_id": "1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving audits with a non-existent "object_id" returns an empty list + response = self.client.get( + reverse("api-v2:audit-list"), + {"object_id": "-1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving audits whose "user" is "demo" + expected = tests.Data("examples", "rest.audit.filter_by_user.json").json() + response = self.client.get( + reverse("api-v2:audit-list"), + {"user": "demo"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving audits with a non-existent "user" returns an empty list + response = self.client.get( + reverse("api-v2:audit-list"), + {"user": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_notifier(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving notifiers without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:sender-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all notifiers + expected = tests.Data("examples", "rest.notifier.default.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated notifiers + expected = tests.Data("examples", "rest.notifier.paginated.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers whose "content_type" is "service" + expected = tests.Data("examples", "rest.notifier.filter_by_content_type.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"content_type": "service"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers with a non-allowed "content_type" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:sender-list"), + {"content_type": "non-allowed"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving notifiers whose "object_id" is "1" + expected = tests.Data("examples", "rest.notifier.filter_by_object_id.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"object_id": "1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers with a non-existent "object_id" returns an empty list + response = self.client.get( + reverse("api-v2:sender-list"), + {"object_id": "-1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving notifiers whose "owner" is "demo" + expected = tests.Data("examples", "rest.notifier.filter_by_owner.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"owner": "demo"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers with a non-existent "owner" returns an empty list + response = self.client.get( + reverse("api-v2:sender-list"), + {"owner": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving notifiers whose "sender" is "promgen.notification.email" + expected = tests.Data("examples", "rest.notifier.filter_by_sender.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"sender": "promgen.notification.email"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers with a non-existent "sender" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:sender-list"), + {"sender": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving notifiers whose "value" contains "general" + expected = tests.Data("examples", "rest.notifier.filter_by_value.json").json() + response = self.client.get( + reverse("api-v2:sender-list"), + {"value": "services"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving notifiers with a non-existent "value" returns an empty list + response = self.client.get( + reverse("api-v2:sender-list"), + {"value": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check update a notifier without token returns 401 Unauthorized + response = self.client.put( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check update a notifier without permission returns 403 Forbidden + response = self.client.put( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check partial update a notifier without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check partial update a notifier without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check add filter for a notifier without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:sender-add-filter", args=[1]), + {"name": "name", "value": "value"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check add filter for a notifier without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:sender-add-filter", args=[1]), + {"name": "name", "value": "value"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete filter for a notifier without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:sender-delete-filter", args=[1, 1]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete filter for a notifier without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:sender-delete-filter", args=[1, 1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete a notifier without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:sender-detail", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete a notifier without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:sender-detail", args=[1]), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 403) + + user = User.objects.get(username="demo") + user.user_permissions.add(Permission.objects.get(codename="add_sender")) + user.user_permissions.add(Permission.objects.get(codename="change_sender")) + user.user_permissions.add(Permission.objects.get(codename="delete_sender")) + + # Check update a notifier successfully with permission + notifier = models.Sender.objects.get(id=1) + notifier.enabled = True + response = self.client.put( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + notifier.refresh_from_db() + self.assertFalse(notifier.enabled) + + # Check partial update a notifier successfully with permission + notifier = models.Sender.objects.get(id=1) + notifier.enabled = True + response = self.client.patch( + reverse("api-v2:sender-detail", args=[1]), + {"enabled": False}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + notifier.refresh_from_db() + self.assertFalse(notifier.enabled) + + # Check add filter for a notifier successfully with permission + response = self.client.post( + reverse("api-v2:sender-add-filter", args=[2]), + {"name": "name", "value": "value"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in response.data["filters"]: + item.pop("id", None) + self.assertEqual(response.data["filters"], [{"name": "name", "value": "value"}]) + + # Check delete filter for a notifier successfully with permission + response = self.client.delete( + reverse("api-v2:sender-delete-filter", args=[2, 2]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + # Check delete a notifier successfully with permission + response = self.client.delete( + reverse("api-v2:sender-detail", args=[1]), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 204) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_rule(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving all rules + expected = tests.Data("examples", "rest.rule.default.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated rules + expected = tests.Data("examples", "rest.rule.paginated.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving rules whose "content_type" is "service" + expected = tests.Data("examples", "rest.rule.filter_by_content_type.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + {"content_type": "service"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving rules with a non-allowed "content_type" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:rule-list"), + {"content_type": "non-allowed"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving rules whose "object_id" is "1" + expected = tests.Data("examples", "rest.rule.filter_by_object_id.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + {"object_id": "1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving rules with a non-existent "object_id" returns an empty list + response = self.client.get( + reverse("api-v2:rule-list"), + {"object_id": "-1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving rules whose "enabled" is "true" + expected = tests.Data("examples", "rest.rule.filter_by_enabled.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + {"enabled": "true"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), expected) + + # Check retrieving rules whose "parent" is "Test Rule" + expected = tests.Data("examples", "rest.rule.filter_by_parent.json").json() + response = self.client.get( + reverse("api-v2:rule-list"), + {"parent": "Test Rule"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving rules with a non-existent "parent" returns an empty list + response = self.client.get( + reverse("api-v2:rule-list"), + {"parent": "-1"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving rule's details without token returns 401 Unauthorized + response = self.client.get( + reverse("api-v2:rule-detail", args=[-1]), + ) + self.assertEqual(response.status_code, 401) + + # Check retrieving rule's details with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:rule-detail", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check retrieving rule's details with an existing "id" returns the expected rule + expected = tests.Data("examples", "rest.rule.detail.json").json() + response = self.client.get( + reverse("api-v2:rule-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + + # Check update a rule without token returns 401 Unauthorized + response = self.client.put( + reverse("api-v2:rule-detail", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleUpdated", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check update a rule without permission returns 403 Forbidden + response = self.client.put( + reverse("api-v2:rule-detail", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleUpdated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check partial update a rule without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:rule-detail", args=[2]), + {"enabled": True}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check partial update a rule without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:rule-detail", args=[2]), + {"enabled": True}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete a rule without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:rule-detail", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete a rule without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:rule-detail", args=[1]), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 403) + + user = User.objects.get(username="demo") + user.user_permissions.add(Permission.objects.get(codename="change_rule")) + user.user_permissions.add(Permission.objects.get(codename="delete_rule")) + + # Check update a rule successfully with permission + expected = tests.Data("examples", "rest.rule.update.json").json() + response = self.client.put( + reverse("api-v2:rule-detail", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleUpdated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check partial update a rule successfully with permission + expected = tests.Data("examples", "rest.rule.partial_update.json").json() + response = self.client.patch( + reverse("api-v2:rule-detail", args=[2]), + {"enabled": True}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check delete a rule successfully with permission + response = self.client.delete( + reverse("api-v2:rule-detail", args=[1]), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 204) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_exporter(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving exporters without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:exporter-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all exporters + expected = tests.Data("examples", "rest.exporter.default.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated exporters + expected = tests.Data("examples", "rest.exporter.paginated.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters whose "enabled" is "true" + expected = tests.Data("examples", "rest.exporter.filter_by_enabled.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"enabled": "true"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters whose "job" contains "inx" + expected = tests.Data("examples", "rest.exporter.filter_by_job.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"job": "inx"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters with a non-existent "job" returns an empty list + response = self.client.get( + reverse("api-v2:exporter-list"), + {"job": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving exporters whose "path" contains "metrics" + expected = tests.Data("examples", "rest.exporter.filter_by_path.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"path": "/metrics"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters with a non-existent "path" returns an empty list + response = self.client.get( + reverse("api-v2:exporter-list"), + {"path": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving exporters whose "project" contains "test" + expected = tests.Data("examples", "rest.exporter.filter_by_project.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"project": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters with a non-existent "project" returns an empty list + response = self.client.get( + reverse("api-v2:exporter-list"), + {"project": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving exporters whose "scheme" is "http" + expected = tests.Data("examples", "rest.exporter.filter_by_scheme.json").json() + response = self.client.get( + reverse("api-v2:exporter-list"), + {"scheme": "https"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters with a non-existent "scheme" returns an 400 Bad Request + response = self.client.get( + reverse("api-v2:exporter-list"), + {"scheme": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving exporters whose "id" is "1" without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:exporter-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving exporters whose "id" is "1" + expected = tests.Data("examples", "rest.exporter.detail.json").json() + response = self.client.get( + reverse("api-v2:exporter-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving exporters with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:exporter-detail", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_url(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving urls without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:url-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all urls + expected = tests.Data("examples", "rest.url.default.json").json() + response = self.client.get( + reverse("api-v2:url-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated urls + expected = tests.Data("examples", "rest.url.paginated.json").json() + response = self.client.get( + reverse("api-v2:url-list"), + {"page_number": 2, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving urls whose "probe" is "fixture_test" + expected = tests.Data("examples", "rest.url.filter_by_probe.json").json() + response = self.client.get( + reverse("api-v2:url-list"), + {"probe": "fixture_test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving urls with a non-existent "probe" returns 400 Bad Request + response = self.client.get( + reverse("api-v2:url-list"), + {"probe": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 400) + + # Check retrieving urls whose "project" contains "test" + expected = tests.Data("examples", "rest.url.filter_by_project.json").json() + response = self.client.get( + reverse("api-v2:url-list"), + {"project": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving urls with a non-existent "project" returns an empty list + response = self.client.get( + reverse("api-v2:url-list"), + {"project": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + + # Check retrieving urls whose "id" is "1" without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:url-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving urls whose "id" is "1" + expected = tests.Data("examples", "rest.url.detail.json").json() + response = self.client.get( + reverse("api-v2:url-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_project(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving projects without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all projects + expected = tests.Data("examples", "rest.project.default.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated projects + expected = tests.Data("examples", "rest.project.paginated.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects whose "name" contains "test" + expected = tests.Data("examples", "rest.project.filter_by_name.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + {"name": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects with a non-existent "name" returns an empty list + response = self.client.get( + reverse("api-v2:project-list"), + {"name": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving projects whose "owner" is "demo" + expected = tests.Data("examples", "rest.project.filter_by_owner.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + {"owner": "demo"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects with a non-existent "owner" returns an empty list + response = self.client.get( + reverse("api-v2:project-list"), + {"owner": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving projects whose "service" is "test-service" + expected = tests.Data("examples", "rest.project.filter_by_service.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + {"service": "test-service"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects with a non-existent "service" returns an empty list + response = self.client.get( + reverse("api-v2:project-list"), + {"service": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving projects whose "shard" is "test-shard" + expected = tests.Data("examples", "rest.project.filter_by_shard.json").json() + response = self.client.get( + reverse("api-v2:project-list"), + {"shard": "test-shard"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects with a non-existent "shard" returns an empty list + response = self.client.get( + reverse("api-v2:project-list"), + {"shard": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving projects whose "id" is "1" without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving projects whose "id" is "1" + expected = tests.Data("examples", "rest.project.detail.json").json() + response = self.client.get( + reverse("api-v2:project-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving projects with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:project-detail", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check retrieving list of exporters for a project without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-exporters", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of exporters for a project + expected = tests.Data("examples", "rest.project.exporters.json").json() + response = self.client.get( + reverse("api-v2:project-exporters", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving list of urls for a project without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-urls", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of urls for a project + expected = tests.Data("examples", "rest.project.urls.json").json() + response = self.client.get( + reverse("api-v2:project-urls", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving list of rules for a project without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-rules", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of rules for a project + expected = tests.Data("examples", "rest.project.rules.json").json() + response = self.client.get( + reverse("api-v2:project-rules", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving list of notifiers for a project without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:project-notifiers", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of notifiers for a project + expected = tests.Data("examples", "rest.project.notifiers.json").json() + response = self.client.get( + reverse("api-v2:project-notifiers", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check creating a project without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:project-list"), + {"name": "new-project", "service": "test-service", "shard": "test-shard"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check creating a project without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:project-list"), + {"name": "new-project", "service": "test-service", "shard": "test-shard"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check updating a project without token returns 401 Unauthorized + response = self.client.put( + reverse("api-v2:project-detail", args=[1]), + { + "name": "updated-project", + "owner": "demo", + "shard": "test-shard", + "description": "Test Project Description", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check updating a project without permission returns 403 Forbidden + response = self.client.put( + reverse("api-v2:project-detail", args=[1]), + { + "name": "updated-project", + "owner": "demo", + "shard": "test-shard", + "description": "Test Project Description", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check partial updating a project without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:project-detail", args=[1]), + {"name": "updated-project"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check partial updating a project without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:project-detail", args=[1]), + {"name": "updated-project"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register exporter for a project without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:project-exporters", args=[1]), + { + "job": "test-job", + "port": 8080, + "path": "/metrics", + "scheme": "http", + "enabled": True, + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register exporter for a project without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:project-exporters", args=[1]), + { + "job": "test-job", + "port": 8080, + "path": "/metrics", + "scheme": "http", + "enabled": True, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check update exporter for a project without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:project-update-exporter", args=[1, 1]), + { + "enabled": False, + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check update exporter for a project without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:project-update-exporter", args=[1, 1]), + { + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete exporter for a project without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:project-update-exporter", args=[1, 1]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete exporter for a project without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:project-update-exporter", args=[1, 1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register url for a project without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:project-urls", args=[1]), + {"url": "http://test-url", "probe": "test-probe"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register url for a project without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:project-urls", args=[1]), + {"url": "http://test-url", "probe": "test-probe"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check delete url for a project without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:project-delete-url", args=[1, 1]), + ) + self.assertEqual(response.status_code, 401) + + # Check delete url for a project without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:project-delete-url", args=[1, 1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register rule for a project without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:project-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register rule for a project without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:project-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register notifier for a project without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:project-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register notifier for a project without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:project-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check link farm for a project without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:project-link-farm", args=[1]), + {"farm": "test-farm", "source": "promgen"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check link farm for a project without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:project-link-farm", args=[1]), + {"farm": "test-farm", "source": "promgen"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check unlink farm for a project without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:project-unlink-farm", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check unlink farm for a project without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:project-unlink-farm", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check deleting a project without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:project-detail", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check deleting a project without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:project-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + user = User.objects.get(username="demo") + user.user_permissions.add(Permission.objects.get(codename="add_project")) + user.user_permissions.add(Permission.objects.get(codename="change_project")) + user.user_permissions.add(Permission.objects.get(codename="delete_project")) + + # Check creating a project successfully with permission + expected = tests.Data("examples", "rest.project.create.json").json() + before_count = models.Project.objects.count() + response = self.client.post( + reverse("api-v2:project-list"), + {"name": "new-project", "service": "test-service", "shard": "test-shard"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + after_count = models.Project.objects.count() + self.assertEqual(response.status_code, 201) + self.assertEqual(before_count + 1, after_count) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + expected.pop("id", None) + response.json().pop("id", None) + self.assertEqual(response.json(), expected) + + # Check updating a project successfully with permission + expected = tests.Data("examples", "rest.project.update.json").json() + response = self.client.put( + reverse("api-v2:project-detail", args=[1]), + { + "name": "updated-project", + "owner": "demo", + "shard": "test-shard", + "description": "Test Project Description", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check partial updating a project successfully with permission + expected = tests.Data("examples", "rest.project.partial_update.json").json() + response = self.client.patch( + reverse("api-v2:project-detail", args=[1]), + {"name": "updated-project"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check register exporter for a project successfully with permission + expected = tests.Data("examples", "rest.project.register_exporter.json").json() + response = self.client.post( + reverse("api-v2:project-exporters", args=[1]), + { + "job": "test-job", + "port": 8080, + "path": "/metrics", + "scheme": "http", + "enabled": True, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + for item in response.json(): + item.pop("id", None) + self.assertEqual(response.json(), expected) + + # Check update exporter for a project successfully with permission + expected = tests.Data("examples", "rest.project.update_exporter.json").json() + response = self.client.patch( + reverse("api-v2:project-update-exporter", args=[1, 1]), + { + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + for item in response.json(): + item.pop("id", None) + self.assertEqual(response.json(), expected) + + # Check delete exporter for a project successfully with permission + response = self.client.delete( + reverse("api-v2:project-update-exporter", args=[1, 1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + # Check register url for a project successfully with permission + expected = tests.Data("examples", "rest.project.register_url.json").json() + response = self.client.post( + reverse("api-v2:project-urls", args=[1]), + {"url": "http://test-url", "probe": "http_2xx"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), expected) + + # Check delete url for a project successfully with permission + response = self.client.delete( + reverse("api-v2:project-delete-url", args=[1, 1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + # Check register rule for a project successfully with permission + expected = tests.Data("examples", "rest.project.register_rule.json").json() + response = self.client.post( + reverse("api-v2:project-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + item["annotations"].pop("rule", None) + for item in response.json(): + item.pop("id", None) + item["annotations"].pop("rule", None) + self.assertEqual(response.json(), expected) + + # Check register notifier for a project successfully with permission + expected = tests.Data("examples", "rest.project.register_notifier.json").json() + response = self.client.post( + reverse("api-v2:project-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + for filter in item["filters"]: + filter.pop("id", None) + for item in response.json(): + item.pop("id", None) + for filter in item["filters"]: + filter.pop("id", None) + self.assertEqual(response.json(), expected) + + # Check link farm for a project successfully with permission + expected = tests.Data("examples", "rest.project.link_farm.json").json() + response = self.client.patch( + reverse("api-v2:project-link-farm", args=[1]), + {"farm": "test-farm", "source": "promgen"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check unlink farm for a project successfully with permission + expected = tests.Data("examples", "rest.project.unlink_farm.json").json() + response = self.client.patch( + reverse("api-v2:project-unlink-farm", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check deleting a project successfully with permission + response = self.client.delete( + reverse("api-v2:project-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_service(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving services without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:service-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all services + expected = tests.Data("examples", "rest.service.default.json").json() + response = self.client.get( + reverse("api-v2:service-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated services + expected = tests.Data("examples", "rest.service.paginated.json").json() + response = self.client.get( + reverse("api-v2:service-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving services whose "name" contains "test" + expected = tests.Data("examples", "rest.service.filter_by_name.json").json() + response = self.client.get( + reverse("api-v2:service-list"), + {"name": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving services with a non-existent "name" returns an empty list + response = self.client.get( + reverse("api-v2:service-list"), + {"name": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving services whose "owner" is "demo" + expected = tests.Data("examples", "rest.service.filter_by_owner.json").json() + response = self.client.get( + reverse("api-v2:service-list"), + {"owner": "demo"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving services with a non-existent "owner" returns an empty list + response = self.client.get( + reverse("api-v2:service-list"), + {"owner": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving services whose "id" is "1" without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:service-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving services whose "id" is "1" + expected = tests.Data("examples", "rest.service.detail.json").json() + response = self.client.get( + reverse("api-v2:service-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving services with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:service-detail", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check retrieving list of projects for a service without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:service-projects", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of projects for a service + expected = tests.Data("examples", "rest.service.projects.json").json() + response = self.client.get( + reverse("api-v2:service-projects", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving list of notifiers for a service without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:service-notifiers", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of notifiers for a service + expected = tests.Data("examples", "rest.service.notifiers.json").json() + response = self.client.get( + reverse("api-v2:service-notifiers", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving list of rules for a service without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:service-rules", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of rules for a service + expected = tests.Data("examples", "rest.service.rules.json").json() + response = self.client.get( + reverse("api-v2:service-rules", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check creating a service without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:service-list"), + {"name": "new-service", "owner": "demo", "description": "Test New Service Description"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check creating a service without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:service-list"), + {"name": "new-service", "owner": "demo", "description": "Test New Service Description"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check updating a service without token returns 401 Unauthorized + response = self.client.put( + reverse("api-v2:service-detail", args=[1]), + {"name": "updated-service", "owner": "demo", "description": "Test Updated Description"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check updating a service without permission returns 403 Forbidden + response = self.client.put( + reverse("api-v2:service-detail", args=[1]), + {"name": "updated-service", "owner": "demo", "description": "Test Updated Description"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check partial updating a service without token returns 401 Unauthorized + response = self.client.patch( + reverse("api-v2:service-detail", args=[1]), + {"name": "updated-service"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check partial updating a service without permission returns 403 Forbidden + response = self.client.patch( + reverse("api-v2:service-detail", args=[1]), + {"name": "updated-service"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register project for a service without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:service-projects", args=[1]), + {"name": "new-project", "owner": "demo", "shard": "test-shard"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register project for a service without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:service-projects", args=[1]), + {"name": "new-project", "owner": "demo", "shard": "test-shard"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register notifier for a service without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:service-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register notifier for a service without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:service-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check register rule for a service without token returns 401 Unauthorized + response = self.client.post( + reverse("api-v2:service-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, 401) + + # Check register rule for a service without permission returns 403 Forbidden + response = self.client.post( + reverse("api-v2:service-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + # Check deleting a service without token returns 401 Unauthorized + response = self.client.delete( + reverse("api-v2:service-detail", args=[1]), + ) + self.assertEqual(response.status_code, 401) + + # Check deleting a service without permission returns 403 Forbidden + response = self.client.delete( + reverse("api-v2:service-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 403) + + user = User.objects.get(username="demo") + user.user_permissions.add(Permission.objects.get(codename="add_service")) + user.user_permissions.add(Permission.objects.get(codename="change_service")) + user.user_permissions.add(Permission.objects.get(codename="delete_service")) + + # Check creating a service successfully with permission + expected = tests.Data("examples", "rest.service.create.json").json() + response = self.client.post( + reverse("api-v2:service-list"), + {"name": "new-service", "owner": "demo", "description": "Test New Service Description"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json(), expected) + + # Check updating a service successfully with permission + expected = tests.Data("examples", "rest.service.update.json").json() + response = self.client.put( + reverse("api-v2:service-detail", args=[1]), + {"name": "updated-service", "owner": "demo", "description": "Test Updated Description"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check partial updating a service successfully with permission + expected = tests.Data("examples", "rest.service.partial_update.json").json() + response = self.client.patch( + reverse("api-v2:service-detail", args=[1]), + {"name": "partial-updated-service"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check register project for a service successfully with permission + expected = tests.Data("examples", "rest.service.register_project.json").json() + before_count = models.Project.objects.count() + response = self.client.post( + reverse("api-v2:service-projects", args=[1]), + {"name": "new-project", "owner": "demo", "shard": "test-shard"}, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + after_count = models.Project.objects.count() + self.assertEqual(response.status_code, 201) + self.assertEqual(before_count + 1, after_count) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + expected.pop("id", None) + response.json().pop("id", None) + self.assertEqual(response.json(), expected) + + # Check register notifier for a service successfully with permission + expected = tests.Data("examples", "rest.service.register_notifier.json").json() + response = self.client.post( + reverse("api-v2:service-notifiers", args=[1]), + { + "owner": "demo", + "filters": [{"name": "test-name", "value": "test-value"}], + "sender": "promgen.notification.slack", + "value": "https://test.slack.com", + "enabled": False, + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + for filter in item["filters"]: + filter.pop("id", None) + for item in response.json(): + item.pop("id", None) + for filter in item["filters"]: + filter.pop("id", None) + self.assertEqual(response.json(), expected) + + # Check register rule for a service successfully with permission + expected = tests.Data("examples", "rest.service.register_rule.json").json() + response = self.client.post( + reverse("api-v2:service-rules", args=[1]), + { + "annotations": {"summary": "Test Rule Summary"}, + "clause": "up == 1", + "description": "Test Rule Description", + "duration": "5m", + "enabled": False, + "labels": {"severity": "critical"}, + "name": "TestRuleCreated", + }, + content_type="application/json", + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 201) + # skip comparing the ID + # and make sure the rest of the input from the request is the same as the output + for item in expected: + item.pop("id", None) + item["annotations"].pop("rule", None) + for item in response.json(): + item.pop("id", None) + item["annotations"].pop("rule", None) + self.assertEqual(response.json(), expected) + + # Check deleting a service successfully with permission + response = self.client.delete( + reverse("api-v2:service-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 204) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_rest_shard(self): + token = Token.objects.filter(user__username="demo").first().key + + # Check retrieving shards without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:shard-list")) + self.assertEqual(response.status_code, 401) + + # Check retrieving all shards + expected = tests.Data("examples", "rest.shard.default.json").json() + response = self.client.get( + reverse("api-v2:shard-list"), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving paginated shards + expected = tests.Data("examples", "rest.shard.paginated.json").json() + response = self.client.get( + reverse("api-v2:shard-list"), + {"page_number": 1, "page_size": 1}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving shards whose "name" contains "test" + expected = tests.Data("examples", "rest.shard.filter_by_name.json").json() + response = self.client.get( + reverse("api-v2:shard-list"), + {"name": "test"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving shards with a non-existent "name" returns an empty list + response = self.client.get( + reverse("api-v2:shard-list"), + {"name": "non-existent"}, + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["count"], 0) + + # Check retrieving shards whose "id" is "1" without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:shard-detail", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving shards whose "id" is "1" + expected = tests.Data("examples", "rest.shard.detail.json").json() + response = self.client.get( + reverse("api-v2:shard-detail", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + # Check retrieving shards with a non-existent "id" returns 404 Not Found + response = self.client.get( + reverse("api-v2:shard-detail", args=[-1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 404) + + # Check retrieving list of projects for a shard without token returns 401 Unauthorized + response = self.client.get(reverse("api-v2:shard-projects", args=[1])) + self.assertEqual(response.status_code, 401) + + # Check retrieving list of projects for a shard + expected = tests.Data("examples", "rest.shard.projects.json").json() + response = self.client.get( + reverse("api-v2:shard-projects", args=[1]), + HTTP_AUTHORIZATION=f"Token {token}", + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), expected) + + @override_settings(PROMGEN=tests.SETTINGS) + def test_throttling(self): + # Check throttling for authenticated users + token = Token.objects.filter(user__username="demo").first().key + for _ in range(1000): + response = self.client.get( + reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 200) + response = self.client.get( + reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 429) + + # Check changing rate + models.SiteConfiguration.objects.get_or_create( + key="THROTTLE_RATES", value={"user": "3/day"} + ) + for _ in range(3): + response = self.client.get( + reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 200) + response = self.client.get( + reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}" + ) + self.assertEqual(response.status_code, 429) diff --git a/promgen/tests/test_routes.py b/promgen/tests/test_routes.py index 32f05f2df..d81f46136 100644 --- a/promgen/tests/test_routes.py +++ b/promgen/tests/test_routes.py @@ -31,7 +31,7 @@ def test_import(self, mock_write, mock_reload): self.assertCount(models.Service, 3, "Import one service (Fixture has two services)") self.assertCount(models.Project, 4, "Import two projects") self.assertCount(models.Exporter, 4, "Import two more exporters") - self.assertCount(models.Host, 3, "Import three hosts") + self.assertCount(models.Host, 4, "Import three hosts (Fixture has one host)") @override_settings(PROMGEN=TEST_SETTINGS) @override_settings(CELERY_TASK_ALWAYS_EAGER=True) @@ -52,9 +52,9 @@ def test_replace(self, mock_write, mock_reload): self.assertCount(models.Project, 4, "Import two projects (Fixture has 2 projectsa)") self.assertCount(models.Exporter, 4, "Import two more exporters") self.assertCount( - models.Farm, 4, "Original two farms and one new farm (fixture has one farm)" + models.Farm, 5, "Original three farms and one new farm (fixture has one farm)" ) - self.assertCount(models.Host, 5, "Original 3 hosts and two new ones") + self.assertCount(models.Host, 6, "Original 4 hosts and two new ones") @mock.patch("requests.get") def test_scrape(self, mock_get): diff --git a/promgen/urls.py b/promgen/urls.py index 70b69252f..d491fb19b 100644 --- a/promgen/urls.py +++ b/promgen/urls.py @@ -19,9 +19,11 @@ from django.contrib import admin from django.urls import include, path from django.views.decorators.csrf import csrf_exempt +from drf_spectacular.views import SpectacularAPIView from rest_framework import routers -from promgen import proxy, rest, views +from promgen import proxy, rest, rest_v2, views +from promgen.schemas import SpectacularRapiDocView router = routers.DefaultRouter() router.register("all", rest.AllViewSet, basename="all") @@ -30,6 +32,17 @@ router.register("project", rest.ProjectViewSet) router.register("farm", rest.FarmViewSet) +v2_router = routers.DefaultRouter() +v2_router.register("users", rest_v2.UserViewSet) +v2_router.register("logs", rest_v2.AuditViewSet) +v2_router.register("notifiers", rest_v2.NotifierViewSet) +v2_router.register("rules", rest_v2.RuleViewSet) +v2_router.register("farms", rest_v2.FarmViewSet) +v2_router.register("exporters", rest_v2.ExporterViewSet) +v2_router.register("urls", rest_v2.URLViewSet) +v2_router.register("projects", rest_v2.ProjectViewSet) +v2_router.register("services", rest_v2.ServiceViewSet) +v2_router.register("shards", rest_v2.ShardViewSet) urlpatterns = [ path("admin/", admin.site.urls), @@ -93,6 +106,9 @@ path("audit", views.AuditList.as_view(), name="audit-list"), path("site", views.SiteDetail.as_view(), name="site-detail"), path("profile", views.Profile.as_view(), name="profile"), + path("profile/token/new", views.ProfileTokenCreate.as_view(), name="token-new"), + path("profile/token/delete", views.ProfileTokenDelete.as_view(), name="token-delete"), + path("profile/token/regenerate", views.ProfileTokenRegenerate.as_view(), name="token-regenerate"), path("import", views.Import.as_view(), name="import"), path("import/rules", views.RuleImport.as_view(), name="rule-import"), path("search", views.Search.as_view(), name="search"), @@ -126,6 +142,9 @@ path("proxy/v1/silences/", csrf_exempt(proxy.ProxyDeleteSilence.as_view()), name="proxy-silence-delete"), # Promgen rest API path("rest/", include((router.urls, "api"), namespace="api")), + path("rest/v2/", include((v2_router.urls, "api-v2"), namespace="api-v2")), + path("rest/v2/schema/", SpectacularAPIView.as_view(), name="api-v2-schema"), + path("rest/v2/api-specs/", SpectacularRapiDocView.as_view(url_name="api-v2-schema"), name="api-v2-specs"), # PromQL Query path("promql-query", views.PromqlQuery.as_view(), name="promql-query"), ] diff --git a/promgen/util.py b/promgen/util.py index 135e9ed73..09a337be2 100644 --- a/promgen/util.py +++ b/promgen/util.py @@ -8,6 +8,9 @@ from django.conf import settings from django.db.models import F from django.http import HttpResponse +from rest_framework import throttling + +from promgen import models # Wrappers around request api to ensure we always attach our user agent # https://github.com/requests/requests/blob/master/requests/api.py @@ -143,3 +146,11 @@ def proxy_error(response: requests.Response) -> HttpResponse: get.__doc__ = requests.get.__doc__ post.__doc__ = requests.post.__doc__ delete.__doc__ = requests.delete.__doc__ + + +class UserRateThrottle(throttling.UserRateThrottle): + def get_rate(self): + rate = models.SiteConfiguration.objects.filter(key="THROTTLE_RATES").first() + if rate and rate.value["user"]: + return rate.value["user"] + return super().get_rate() diff --git a/promgen/views.py b/promgen/views.py index 682805f3a..78a30dc80 100644 --- a/promgen/views.py +++ b/promgen/views.py @@ -31,6 +31,7 @@ from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily from prometheus_client.parser import text_string_to_metric_families from requests.exceptions import HTTPError +from rest_framework.authtoken.models import Token import promgen.templatetags.promgen as macro from promgen import ( @@ -973,6 +974,7 @@ def get_context_data(self, **kwargs): context["subscriptions"] = models.Sender.objects.filter( sender="promgen.notification.user", value=self.request.user.username ) + context["api_token"] = Token.objects.filter(user=self.request.user).first() return context def form_valid(self, form): @@ -1461,3 +1463,22 @@ def get(self, request): return util.proxy_error(response) return HttpResponse(response.content, content_type="application/json") + + +class ProfileTokenCreate(LoginRequiredMixin, View): + def post(self, request): + Token.objects.create(user=self.request.user) + return redirect("profile") + + +class ProfileTokenDelete(LoginRequiredMixin, View): + def post(self, request): + Token.objects.filter(user=self.request.user).delete() + return redirect("profile") + + +class ProfileTokenRegenerate(LoginRequiredMixin, View): + def post(self, request): + Token.objects.filter(user=self.request.user).delete() + Token.objects.create(user=self.request.user) + return redirect("profile") diff --git a/pyproject.toml b/pyproject.toml index d5370e8d4..22dce6eed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ dependencies = [ "django-environ", "django-filter", "djangorestframework", + "drf-spectacular", "kombu", "prometheus-client", "python-dateutil",