Skip to content

api ¤

The api app exposes the API endpoints for the application.

It uses the rest_framework package to provide the API endpoints.

admin ¤

APITokenAdmin ¤

Bases: TokenAdmin

Custom Admin for DRF Token. Add supports for custom permissions.

formfield_for_foreignkey ¤

formfield_for_foreignkey(db_field: ForeignKey[Any, Any], request: HttpRequest, **kwargs: Any) -> ModelChoiceField

Show all or only current user depending on permissions.

Source code in src/firefighter/api/admin.py
def formfield_for_foreignkey(self, db_field: ForeignKey[Any, Any], request: HttpRequest, **kwargs: Any) -> ModelChoiceField:  # type: ignore[override]
    """Show all or only current user depending on permissions."""
    if db_field.name == "user":
        if request.user.has_perm("api.can_add_any") or request.user.has_perm(
            "api.can_edit_any"
        ):
            kwargs["queryset"] = User.objects.all()
        elif request.user.has_perm("api.can_add_own"):
            kwargs["queryset"] = User.objects.filter(id=request.user.id)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)

get_form ¤

get_form(request: HttpRequest, obj: APITokenProxy | None = None, change: bool = False, **kwargs: Any) -> type[ModelForm[APITokenProxy]]

Prefill the form with the current user.

Source code in src/firefighter/api/admin.py
def get_form(self, request: HttpRequest, obj: APITokenProxy | None = None, change: bool = False, **kwargs: Any) -> type[ModelForm[APITokenProxy]]:  # type: ignore[override] # noqa: FBT001, FBT002
    """Prefill the form with the current user."""
    form: type[ModelForm[APITokenProxy]] = super().get_form(
        request, obj, change, **kwargs
    )
    form.base_fields["user"].initial = request.user
    return form

get_queryset ¤

get_queryset(request: HttpRequest) -> QuerySet[APITokenProxy]

Show all or only own tokens depending on permissions.

Source code in src/firefighter/api/admin.py
def get_queryset(self, request: HttpRequest) -> QuerySet[APITokenProxy]:  # type: ignore[override]
    """Show all or only own tokens depending on permissions."""
    qs = super().get_queryset(request)
    if request.user.has_perm("api.can_view_any"):
        return qs
    return qs.filter(user=request.user)

get_sortable_by ¤

get_sortable_by(request: HttpRequest)

Hack to send a message depending on the status of the user.

Source code in src/firefighter/api/admin.py
def get_sortable_by(self, request: HttpRequest):  # type: ignore[no-untyped-def,override]
    """Hack to send a message depending on the status of the user."""
    if request.user.has_perm("api.can_view_any"):
        self.message_user(request, "You are seeing all tokens.", messages.WARNING)
    elif request.user.has_perm("api.can_view_own"):
        self.message_user(
            request, "You are only seeing your tokens.", messages.WARNING
        )
    return super().get_sortable_by(request)

authentication ¤

BearerTokenAuthentication ¤

Bases: TokenAuthentication

To use Authorization: Bearer <token> instead of Authorization: Token <token>.

models ¤

APITokenProxy ¤

Bases: APIToken

Proxy mapping pk to user pk for use in admin.

Overrides default permissions.

permissions ¤

StrictDjangoModelPermissions ¤

Bases: DjangoModelPermissions

Custom class to restrict GET requests.

renderer ¤

CSVRenderer ¤

Bases: CSVRenderer

Renderer which serializes to CSV Override the default CSV Renderer to allow hiding header fields. Hide head by setting labels to "hidden".

tablize ¤

tablize(data: Any, header: Any | None = None, labels: Any | None = None) -> Generator[list[Any], None, None]

Convert a list of data into a table.

If there is a header provided to tablize it will efficiently yield each row as needed. If no header is provided, tablize will need to process each row in the data in order to construct a complete header. Thus, if you have a lot of data and want to stream it, you should probably provide a header to the renderer (using the header attribute, or via the renderer_context).

Source code in src/firefighter/api/renderer.py
def tablize(
    self, data: Any, header: Any | None = None, labels: Any | None = None
) -> Generator[list[Any], None, None]:
    """Convert a list of data into a table.

    If there is a header provided to tablize it will efficiently yield each
    row as needed. If no header is provided, tablize will need to process
    each row in the data in order to construct a complete header. Thus, if
    you have a lot of data and want to stream it, you should probably
    provide a header to the renderer (using the `header` attribute, or via
    the `renderer_context`).
    """
    # Try to pull the header off of the data, if it's not passed in as an
    # argument.
    if not header and hasattr(data, "header"):
        header = data.header

    if data:
        # First, flatten the data (i.e., convert it to a list of
        # dictionaries that are each exactly one level deep).  The key for
        # each item designates the name of the column that the item will
        # fall into.
        data = self.flatten_data(data)

        # Get the set of all unique headers, and sort them (unless already provided).
        data, header = self._get_headers(data, header)

        # Return your "table", with the headers as the first row.
        if labels != "__hidden__":
            if labels:
                yield [labels.get(x, x) for x in header]
            else:
                yield header

        # Create a row for each dictionary, filling in columns for which the
        # item has no data with None values.
        for item in data:
            row = [item.get(key, None) for key in header]
            yield row

    elif header:
        # If there's no data but a header was supplied, yield the header.
        if labels:
            yield [labels.get(x, x) for x in header]
        else:
            yield header

    else:
        # Generator will yield nothing if there's no data and no header
        pass

TSVRenderer ¤

Bases: CSVRenderer

Renderer which serializes to TSV.

tablize ¤

tablize(data: Any, header: Any | None = None, labels: Any | None = None) -> Generator[list[Any], None, None]

Convert a list of data into a table.

If there is a header provided to tablize it will efficiently yield each row as needed. If no header is provided, tablize will need to process each row in the data in order to construct a complete header. Thus, if you have a lot of data and want to stream it, you should probably provide a header to the renderer (using the header attribute, or via the renderer_context).

Source code in src/firefighter/api/renderer.py
def tablize(
    self, data: Any, header: Any | None = None, labels: Any | None = None
) -> Generator[list[Any], None, None]:
    """Convert a list of data into a table.

    If there is a header provided to tablize it will efficiently yield each
    row as needed. If no header is provided, tablize will need to process
    each row in the data in order to construct a complete header. Thus, if
    you have a lot of data and want to stream it, you should probably
    provide a header to the renderer (using the `header` attribute, or via
    the `renderer_context`).
    """
    # Try to pull the header off of the data, if it's not passed in as an
    # argument.
    if not header and hasattr(data, "header"):
        header = data.header

    if data:
        # First, flatten the data (i.e., convert it to a list of
        # dictionaries that are each exactly one level deep).  The key for
        # each item designates the name of the column that the item will
        # fall into.
        data = self.flatten_data(data)

        # Get the set of all unique headers, and sort them (unless already provided).
        data, header = self._get_headers(data, header)

        # Return your "table", with the headers as the first row.
        if labels != "__hidden__":
            if labels:
                yield [labels.get(x, x) for x in header]
            else:
                yield header

        # Create a row for each dictionary, filling in columns for which the
        # item has no data with None values.
        for item in data:
            row = [item.get(key, None) for key in header]
            yield row

    elif header:
        # If there's no data but a header was supplied, yield the header.
        if labels:
            yield [labels.get(x, x) for x in header]
        else:
            yield header

    else:
        # Generator will yield nothing if there's no data and no header
        pass

serializers ¤

CreatableSlugRelatedField ¤

Bases: SlugRelatedField[T], Generic[T]

Like SlugRelatedField, but allows to create an object that is not present in DB.

run_validation ¤

run_validation(data: Any = empty) -> T | Any | None

Override the default validation to perform the validation before trying to create the object.

Source code in src/firefighter/api/serializers.py
def run_validation(self, data: Any = empty) -> T | Any | None:
    """Override the default validation to perform the validation before trying to create the object."""
    (is_empty_value, data) = self.validate_empty_values(data)
    if is_empty_value:
        return data
    self.run_validators(data)
    return self.to_internal_value(data)

GroupedModelSerializer ¤

GroupedModelSerializer(
    child_serializer: type[serializers.ModelSerializer[T]], key_getter: Callable[[T], str] = lambda: str(x.pk), **kwargs: Any
)

Bases: RelatedField[T, Any, Any], Generic[T]

Generic implementation for a model with a One2Many, where instead of a list we want the items grouped by a field, like costs or metrics.

Source code in src/firefighter/api/serializers.py
def __init__(
    self,
    child_serializer: type[serializers.ModelSerializer[T]],
    key_getter: Callable[[T], str] = lambda x: str(x.pk),
    **kwargs: Any,
):
    self.child_serializer = child_serializer
    self.key_getter = key_getter
    if not kwargs.get("read_only", True):
        raise ValueError("GroupedModelSerializer can only be read_only")
    if "read_only" not in kwargs:
        kwargs["read_only"] = True
    kwargs["many"] = True
    super().__init__(**kwargs)

urls ¤

views ¤

components ¤

incident_cost_types ¤

incident_costs ¤

incidents ¤

CreateIncidentViewSet ¤

Bases: CreateModelMixin, GenericViewSet[Incident]

create ¤
create(request: Request, *args: Never, **kwargs: Never) -> Response

Allow to create an incident. Requires a valid Bearer token, that you can create in the back-office if you have the right permissions.

Source code in src/firefighter/api/views/incidents.py
def create(self, request: Request, *args: Never, **kwargs: Never) -> Response:
    """Allow to create an incident.
    Requires a valid Bearer token, that you can create in the back-office if you have the right permissions.
    """
    serializer: BaseSerializer[Incident] = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    self.perform_create(serializer)
    headers = self.get_success_headers(serializer.data)
    return ProcessAfterResponse(
        serializer.data, status=status.HTTP_201_CREATED, headers=headers
    )

IncidentViewSet ¤

Bases: RetrieveModelMixin, ListModelMixin, AdvancedGenericViewSet[Incident]

labels class-attribute instance-attribute ¤
labels: dict[str, str] = {}

Dict mapping attributes/properties to a more friendly string to be shown in the header row for CSV/TSV renders.

E.g. labels = {"slack_channel_name": "Slack Channel"}

list ¤
list(request: Request, *args: Never, **kwargs: Never) -> Response

List all incidents. Allows to show or hide tags which is DB intensive, with the show_tags parameters.

status and _status__* uses an enum:

10, "Open"
20, "Investigating"
30, "Mitigating"
40, "Mitigated"
50, "Post-mortem"
60, "Closed"

Source code in src/firefighter/api/views/incidents.py
def list(self, request: Request, *args: Never, **kwargs: Never) -> Response:
    """List all incidents. Allows to show or hide tags which is DB intensive, with the show_tags parameters.

    `status` and `_status__*` uses an enum:
    ```
    10, "Open"
    20, "Investigating"
    30, "Mitigating"
    40, "Mitigated"
    50, "Post-mortem"
    60, "Closed"
    ```
    """
    queryset = self.filter_queryset(self.get_queryset())
    serializer: BaseSerializer[Incident]
    if request.query_params.get("show_tags") == "true":
        serializer = IncidentSerializer(
            queryset, many=True, context={"remove_fields": []}
        )
    else:
        serializer = self.get_serializer(
            queryset, many=True, context={"remove_fields": ["tags"]}
        )
    return Response(serializer.data)

ProcessAfterResponse ¤

Bases: Response

Custom DRF Response, to trigger the Slack workflow after creating the incident and returning HTTP 201.

severities ¤