Skip to content

firefighter ¤

This is the main entrypoint for FireFighter.

Warning

This app does not contain any business logic. It is responsible for loading all the settings, providing common base features and tying together all the other apps.

Features¤

  • Load all settings
  • Provide ASGI and WSGI entrypoints
  • Configure loggers
  • Provide a Celery app
  • Set the base URL routing
  • Provides a healthcheck endpoint
  • Provide a robots.txt endpoint
  • Provides some utils
  • Provides SSO integration
  • Register the Django admin and customize its theme

apps ¤

asgi ¤

ASGI config for firefighter project.

It exposes the ASGI callable as a module-level variable named application.

For more information on this file, see https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/

fields_forms_widgets ¤

CustomCheckboxSelectMultiple ¤

Bases: CheckboxSelectMultiple

Custom template.

FFDateRangeSingleFilter ¤

Bases: Filter

TODO Fix typings, implement tests, move in proper location.

GroupedCheckboxSelectMultiple ¤

Bases: CustomCheckboxSelectMultiple

Widget to group checkboxes in a select multiple. TODO Make this generic!

optgroups ¤

optgroups(name: str, value: list[Any], attrs: dict[str, Any] | None = None) -> list[tuple[str | None, list[dict[str, Any]], int | None]]

Return a list of optgroups for this widget.

Source code in src/firefighter/firefighter/fields_forms_widgets.py
def optgroups(
    self, name: str, value: list[Any], attrs: dict[str, Any] | None = None
) -> list[tuple[str | None, list[dict[str, Any]], int | None]]:
    """Return a list of optgroups for this widget."""
    self.choices = [
        (key, list(group))
        for key, group in groupby(self.choices, key=lambda x: x[0].instance.group)  # type: ignore[union-attr]
    ]
    return super().optgroups(name, value, attrs)

TextDateRangeField ¤

Bases: Field

to_python ¤

to_python(value: str | None) -> tuple[date | None, date | None, str | None, str | None] | None

Validate date range. Field is optional.

Source code in src/firefighter/firefighter/fields_forms_widgets.py
def to_python(
    self, value: str | None
) -> tuple[date | None, date | None, str | None, str | None] | None:
    """Validate date range. Field is optional."""
    if not value or value.strip() == "":
        return None
    date_range = get_date_range_from_special_date(value)
    if date_range[0] is None and date_range[1] is None:
        err_msg = f"Invalid date range: {value}"
        raise ValidationError(err_msg)
    if (date_range[0] is not None and date_range[1] is not None) and date_range[
        0
    ] > date_range[1]:
        err_msg = f"Start date must be before end date ({date_range[0].strftime('%Y-%m-%d %H:%M:%S')} > {date_range[1].strftime('%Y-%m-%d %H:%M:%S')})."
        raise ValidationError(err_msg)

    return date_range

filters ¤

Collection of custom Django template filters. They are added as built-in filters and thus can be used in templates of any app.

apply_filter ¤

apply_filter(value: Any, fn_holder: dict[Literal['filter_args', 'filter'], Callable[..., V]]) -> V | Any

Applies a filter function to a given value.

Parameters:

  • value (Any) –

    The value to be filtered.

  • fn_holder (dict[Literal['filter_args', 'filter'], Callable[..., V]]) –

    A dictionary containing the filter function and its arguments.

Returns:

  • V | Any

    The filtered value.

Source code in src/firefighter/firefighter/filters.py
@register_global.filter
def apply_filter(
    value: Any,
    fn_holder: dict[Literal["filter_args", "filter"], Callable[..., V]],
) -> V | Any:
    """Applies a filter function to a given value.

    Args:
        value: The value to be filtered.
        fn_holder: A dictionary containing the filter function and its arguments.

    Returns:
        The filtered value.
    """
    fn = fn_holder.get("filter")

    if not fn:
        return value

    args = fn_holder.get("filter_args")
    if args:
        return fn(value, args)
    return fn(value)

get_item ¤

get_item(dictionary: dict[str, Any], key: Any) -> Any

Get the value of a key from a dictionary or object.

Parameters:

  • dictionary (dict[str, Any]) –

    The dictionary or object to get the value from.

  • key (Any) –

    The key to get the value for.

Returns:

  • Any ( Any ) –

    The value of the key in the dictionary or object.

Source code in src/firefighter/firefighter/filters.py
@register_global.filter
def get_item(dictionary: dict[str, Any], key: Any) -> Any:
    """Get the value of a key from a dictionary or object.

    Args:
        dictionary (dict[str, Any]): The dictionary or object to get the value from.
        key (Any): The key to get the value for.

    Returns:
        Any: The value of the key in the dictionary or object.
    """
    if hasattr(dictionary, key):
        return getattr(dictionary, key)
    if hasattr(dictionary, "__getitem__"):
        try:
            return dictionary[key]
        except KeyError:
            pass
    if hasattr(dictionary, "get"):
        return dictionary.get(key)
    if not isinstance(dictionary, dict):
        dictionary = dictionary.__dict__  # type: ignore

    return dictionary.get(key)

markdown ¤

markdown(text: str) -> str

Converts markdown-formatted text to HTML.

Sanitize the HTML to only allow a subset of tags.

Parameters:

  • text (str) –

    The markdown-formatted text to convert.

Returns:

  • str ( str ) –

    The HTML-formatted text.

Source code in src/firefighter/firefighter/filters.py
@register_global.filter
def markdown(text: str) -> str:
    """Converts markdown-formatted text to HTML.

    Sanitize the HTML to only allow a subset of tags.

    Args:
        text (str): The markdown-formatted text to convert.

    Returns:
        str: The HTML-formatted text.
    """
    return nh3.clean(md.markdown(text, output_format="html"), tags=nh3.ALLOWED_TAGS)

readable_time_delta ¤

readable_time_delta(delta: timedelta) -> str

Format a time delta as a string. Ignore seconds and microseconds From https://github.com/wimglenn/readabledelta/blob/master/readabledelta.py (The Unlicense).

Source code in src/firefighter/firefighter/filters.py
@register_global.filter
def readable_time_delta(delta: timedelta) -> str:
    """Format a time delta as a string. Ignore seconds and microseconds
    From https://github.com/wimglenn/readabledelta/blob/master/readabledelta.py (The Unlicense).
    """
    negative = delta < timedelta(0)
    delta = abs(delta)

    # Other allowed values are  "seconds", "milliseconds", "microseconds".
    keys = [
        "weeks",
        "days",
        "hours",
        "minutes",
    ]

    # datetime.timedelta are normalized internally in Python to the units days, seconds, microseconds allowing a unique
    # representation.  This is not the only possible basis; the calculations below rebase onto more human friendly keys
    # noinspection PyDictCreation
    data = {}
    # rebase days onto weeks, days
    data["weeks"], data["days"] = divmod(delta.days, 7)
    # rebase seconds onto hours, minutes, seconds
    data["hours"], data["seconds"] = divmod(delta.seconds, 60 * 60)
    data["minutes"], data["seconds"] = divmod(data["seconds"], 60)
    # rebase microseconds onto milliseconds, microseconds

    if data["weeks"] != 0 or data["days"] != 0:
        keys.remove("minutes")
        keys.remove("hours")

    output = [
        f"{data[k]} {k[:-1] if data[k] == 1 else k}" for k in keys if data[k] != 0
    ]

    if not output:
        result = "an instant"
    elif len(output) == 1:
        [result] = output
    else:
        left, right = output[:-1], output[-1:]
        result = ", ".join(left) + " and " + right[0]

    if negative:
        return "-" + result
    return result

timedelta_chop_microseconds ¤

timedelta_chop_microseconds(delta: timedelta) -> timedelta

Removes microseconds from a timedelta object.

Parameters:

  • delta (timedelta) –

    The timedelta object to remove microseconds from.

Returns:

  • timedelta ( timedelta ) –

    A new timedelta object with microseconds removed.

Source code in src/firefighter/firefighter/filters.py
@register_global.filter
def timedelta_chop_microseconds(delta: timedelta) -> timedelta:
    """Removes microseconds from a timedelta object.

    Args:
        delta (timedelta): The timedelta object to remove microseconds from.

    Returns:
        timedelta: A new timedelta object with microseconds removed.
    """
    return delta - timedelta(microseconds=delta.microseconds)

http_client ¤

HttpClient ¤

HttpClient(client_kwargs: dict[str, Any] | None = None)

Base class for HTTP clients. Uses httpx under the hood.

Sets some defaults, including: - a timeout of 15 seconds for the connection and 20 seconds for the read, to avoid hanging indefinitely - access logging

Used by firefighter.confluence.client.ConfluenceClient.

Source code in src/firefighter/firefighter/http_client.py
def __init__(self, client_kwargs: dict[str, Any] | None = None) -> None:
    self._client = httpx.Client(**(client_kwargs or {}))
    self._client.timeout = httpx.Timeout(15, read=20)
    if FF_HTTP_CLIENT_ADDITIONAL_HEADERS:
        self._client.headers = httpx.Headers(
            {
                **self._client.headers,
                **FF_HTTP_CLIENT_ADDITIONAL_HEADERS,
            }
        )

middleware ¤

HeaderUser ¤

HeaderUser(get_response: Callable[[HttpRequest], HttpResponse])

Adds the user ID to the response headers configured with "FF-User-Id", to log in access logs.

Source code in src/firefighter/firefighter/middleware.py
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
    self.get_response = get_response

settings ¤

This module define all the settings for the FireFighter project.

Warning

It should NOT be imported directly from any other module.

To use the settings, import them through Django's django.conf.settings object:

from django.conf import settings

components ¤

api ¤

DRF_STANDARDIZED_ERRORS module-attribute ¤
DRF_STANDARDIZED_ERRORS = {'ALLOWED_ERROR_STATUS_CODES': ['400', '401', '403', '404', '429', '500']}

Additional servers to add to the OpenAPI schema

celery ¤

Celery settings definition.

common ¤

Django settings for server project.

For more information on this file, see https://docs.djangoproject.com/en/4.2/topics/settings/

For the full list of settings and their config, see https://docs.djangoproject.com/en/4.2/ref/settings/

FF_DEBUG_ERROR_PAGES module-attribute ¤
FF_DEBUG_ERROR_PAGES: bool = config('FF_DEBUG_ERROR_PAGES', default=False, cast=bool)

Add routes to display error pages. Useful for debugging.

FF_EXPOSE_API_DOCS module-attribute ¤
FF_EXPOSE_API_DOCS: bool = config('FF_EXPOSE_API_DOCS', default=False, cast=bool)

Expose the API documentation. Useful for debugging. Can be a security issue.

FF_HTTP_CLIENT_ADDITIONAL_HEADERS module-attribute ¤
FF_HTTP_CLIENT_ADDITIONAL_HEADERS: dict[str, Any] | None = None

Additional headers to send with every HTTP request made using our HttpClient. Useful for global auth, or adding a specific User-Agent.

FF_OVERRIDE_MENUS_CREATION module-attribute ¤
FF_OVERRIDE_MENUS_CREATION: Callable[[], None] | None = None

Override the default menus creation. Useful for custom menus.

FF_ROLE_REMINDER_MIN_DAYS_INTERVAL module-attribute ¤
FF_ROLE_REMINDER_MIN_DAYS_INTERVAL = config('FF_ROLE_REMINDER_MIN_DAYS_INTERVAL', default=90, cast=int)

Number of days between role explanation/reminders, for each role. -1 disable the messages, and 0 will send the message everytime.

FF_SKIP_SECRET_KEY_CHECK module-attribute ¤
FF_SKIP_SECRET_KEY_CHECK: bool = config('FF_SKIP_SECRET_KEY_CHECK', default=False, cast=bool)

Skip the SECRET_KEY check. Make sure to set a strong SECRET_KEY in production.

FF_USER_ID_HEADER module-attribute ¤
FF_USER_ID_HEADER: str = 'FF-User-Id'

Header name to add to every HTTP request made using our HttpClient. Useful for logging.

confluence ¤

CONFLUENCE_API_KEY module-attribute ¤
CONFLUENCE_API_KEY: str = config('CONFLUENCE_API_KEY')

The Confluence API key to use.

CONFLUENCE_ON_CALL_PAGE_ID module-attribute ¤
CONFLUENCE_ON_CALL_PAGE_ID: int | None = config('CONFLUENCE_ON_CALL_PAGE_ID', cast=int, default=None)

The Confluence page ID where to export the current on-call schedule. If not set, export tasks will be skipped.

CONFLUENCE_POSTMORTEM_FOLDER_ID module-attribute ¤
CONFLUENCE_POSTMORTEM_FOLDER_ID: int = config('CONFLUENCE_POSTMORTEM_FOLDER_ID', cast=int)

The Confluence page ID where to create and nest postmortems.

CONFLUENCE_POSTMORTEM_SPACE module-attribute ¤
CONFLUENCE_POSTMORTEM_SPACE: str = config('CONFLUENCE_POSTMORTEM_SPACE')

XXX To rename CONFLUENCE_DEFAULT_SPACE. The Confluence space where to create pages by default, mainly for postmortems.

CONFLUENCE_POSTMORTEM_TEMPLATE_PAGE_ID module-attribute ¤
CONFLUENCE_POSTMORTEM_TEMPLATE_PAGE_ID: int = config('CONFLUENCE_POSTMORTEM_TEMPLATE_PAGE_ID', cast=int)

The Confluence page ID of the template to use for postmortems.

CONFLUENCE_RUNBOOKS_FOLDER_ID module-attribute ¤
CONFLUENCE_RUNBOOKS_FOLDER_ID: int = config('CONFLUENCE_RUNBOOKS_FOLDER_ID', cast=int)

The Confluence page ID where runbooks are stored.

CONFLUENCE_URL module-attribute ¤
CONFLUENCE_URL: str = config('CONFLUENCE_URL')

The Confluence URL to use. If no protocol is defined, https will be used.

CONFLUENCE_USERNAME module-attribute ¤
CONFLUENCE_USERNAME: str = config('CONFLUENCE_USERNAME')

The Confluence username to use.

ENABLE_CONFLUENCE module-attribute ¤
ENABLE_CONFLUENCE = config('ENABLE_CONFLUENCE', cast=bool, default=False)

Enable the Confluence app.

jira_app ¤

ENABLE_JIRA module-attribute ¤
ENABLE_JIRA: bool = config('ENABLE_JIRA', cast=bool, default=False)

Enable the Jira app.

RAID_JIRA_API_PASSWORD module-attribute ¤
RAID_JIRA_API_PASSWORD: str = config('RAID_JIRA_API_PASSWORD')

The Jira API password to use.

RAID_JIRA_API_URL module-attribute ¤
RAID_JIRA_API_URL: str = config('RAID_JIRA_API_URL')

The Jira API URL to use. If no protocol is defined, https will be used.

RAID_JIRA_API_USER module-attribute ¤
RAID_JIRA_API_USER: str = config('RAID_JIRA_API_USER')

The Jira API user to use.

logging ¤

AccessLogFilter ¤

Bases: Filter

Filter Gunicorn.access logs to avoid logging static files and healthcheck requests logging.

pagerduty ¤

ENABLE_PAGERDUTY module-attribute ¤
ENABLE_PAGERDUTY: bool = config('ENABLE_PAGERDUTY', cast=bool, default=False)

Enable PagerDuty integration.

PAGERDUTY_ACCOUNT_EMAIL module-attribute ¤
PAGERDUTY_ACCOUNT_EMAIL: str = config('PAGERDUTY_ACCOUNT_EMAIL')

PagerDuty account email, linked to the API key.

PAGERDUTY_API_KEY module-attribute ¤
PAGERDUTY_API_KEY: str = config('PAGERDUTY_API_KEY')

PagerDuty API key.

PAGERDUTY_URL module-attribute ¤
PAGERDUTY_URL: str = config('PAGERDUTY_URL', default='https://api.pagerduty.com')

PagerDuty API URL.

raid ¤

ENABLE_RAID module-attribute ¤
ENABLE_RAID: bool = config('ENABLE_RAID', cast=bool, default=False)

Enable the Raid app. Jira app must be enabled and configured as well.

RAID_DEFAULT_JIRA_QRAFT_USER_ID module-attribute ¤
RAID_DEFAULT_JIRA_QRAFT_USER_ID: str = config('RAID_DEFAULT_JIRA_QRAFT_USER_ID')

The default Jira user ID to use for creating issues

RAID_JIRA_PROJECT_KEY module-attribute ¤
RAID_JIRA_PROJECT_KEY: str = config('RAID_JIRA_PROJECT_KEY')

The Jira project key to use for creating issues, e.g. 'INC'

RAID_JIRA_USER_IDS module-attribute ¤
RAID_JIRA_USER_IDS: dict[str, str] = {}

Mapping of domain to default Jira user ID

RAID_QUALIFIER_URL module-attribute ¤
RAID_QUALIFIER_URL: str = config('RAID_QUALIFIER_URL')

Link to the board with issues to qualify

slack ¤

FF_SLACK_SKIP_CHECKS module-attribute ¤
FF_SLACK_SKIP_CHECKS: bool = config('FF_SLACK_SKIP_CHECKS', cast=bool, default=_is_default_skip_check_cmd)

Skip Slack checks. Only use for testing or demo.

SLACK_APP_EMOJI module-attribute ¤
SLACK_APP_EMOJI: str = config('SLACK_APP_EMOJI', default=':fire_extinguisher:')

Emoji to represent the app in Slack surfaces. Can be an actual emoji, or a string for a custom emoji present in your Workspace, like ":incident_logo:".

SLACK_BOT_TOKEN module-attribute ¤
SLACK_BOT_TOKEN: str = config('SLACK_BOT_TOKEN')

The Slack bot token to use.

SLACK_EMERGENCY_COMMUNICATION_GUIDE_URL module-attribute ¤
SLACK_EMERGENCY_COMMUNICATION_GUIDE_URL: str | None = config('SLACK_EMERGENCY_COMMUNICATION_GUIDE_URL', default=None)

URL to add in the Slack emergency message. Useful to point to your own documentation.

SLACK_EMERGENCY_USERGROUP_ID module-attribute ¤
SLACK_EMERGENCY_USERGROUP_ID: str | None = config('SLACK_EMERGENCY_USERGROUP_ID', default=None)

The Slack usergroup ID to use for emergency notifications. If not set, no group will be mentionned in the message.

SLACK_INCIDENT_COMMAND module-attribute ¤
SLACK_INCIDENT_COMMAND: str = config('SLACK_INCIDENT_COMMAND', default='/incident')

The Slack slash command to use to create and manage incidents.

SLACK_INCIDENT_COMMAND_ALIASES module-attribute ¤
SLACK_INCIDENT_COMMAND_ALIASES: list[str] = config('SLACK_INCIDENT_COMMAND_ALIASES', cast=Csv(), default='')

Comma-separated list of aliases for the incident command.

SLACK_INCIDENT_HELP_GUIDE_URL module-attribute ¤
SLACK_INCIDENT_HELP_GUIDE_URL: str | None = config('SLACK_INCIDENT_HELP_GUIDE_URL', default=None)

URL to add in the Slack help message (/incident help). Useful to point to your own documentation.

SLACK_POSTMORTEM_HELP_URL module-attribute ¤
SLACK_POSTMORTEM_HELP_URL: str | None = config('SLACK_POSTMORTEM_HELP_URL', default=None)

URL to a guide on how to write a postmortem. Useful to point to your own documentation.

SLACK_SEVERITY_HELP_GUIDE_URL module-attribute ¤
SLACK_SEVERITY_HELP_GUIDE_URL: str | None = config('SLACK_SEVERITY_HELP_GUIDE_URL', default=None)

URL to add in the form to choose the priority. Useful to point to your own documentation.

SLACK_SIGNING_SECRET module-attribute ¤
SLACK_SIGNING_SECRET: str = config('SLACK_SIGNING_SECRET')

The Slack signing secret to use.

environments ¤

Overriding settings based on the environment.

dev ¤

This file contains all the settings that defines the development server.

SECURITY WARNING: don't run with debug turned on in production!

prod ¤

This file contains all the settings used in production environments.

This file is required and if ENV=dev these values are not used.

settings_builder ¤

settings_utils ¤

APP_DISPLAY_NAME module-attribute ¤

APP_DISPLAY_NAME: str = config('APP_DISPLAY_NAME', default='FireFighter')

The name of the app. Used in the title of the app, and in the navigation bar.

BASE_URL module-attribute ¤

BASE_URL: str = config('BASE_URL')

The base URL of the app. Used for links in externals surfaces, like Slack or documents.

sso ¤

urls ¤

firefighter URL Configuration.

The urlpatterns list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/4.2/topics/http/urls/

Examples:¤

Function views¤
  1. Add an import: from my_app import views
  2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views¤
  1. Add an import: from other_app.views import Home
  2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf¤
  1. Import the include() function: from django.urls import include, path
  2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))

utils ¤

get_first_in ¤

get_first_in(ulist: list[T], key: str | Sequence[str], matches: Iterable[str], default: V | None = None) -> T | V | None

Returns the first element of a list of dicts, where the value of a key matches one in the provided iterable.

Source code in src/firefighter/firefighter/utils.py
def get_first_in(
    ulist: list[T],
    key: str | Sequence[str],
    matches: Iterable[str],
    default: V | None = None,
) -> T | V | None:
    """Returns the first element of a list of dicts, where the value of a key matches one in the provided iterable."""
    if not isinstance(ulist, list) or ulist is None:
        return default  # type: ignore[unreachable]
    return next(
        (e for e in ulist if get_in(e, key) in matches),
        default,
    )

get_in ¤

get_in(dictionary: dict[str, Any] | Any | None, keys: str | Sequence[str], default: Any | None = None) -> Any

Get a value from arbitrarily nested dicts.

Source code in src/firefighter/firefighter/utils.py
def get_in(
    dictionary: dict[str, Any] | Any | None,
    keys: str | Sequence[str],
    default: Any | None = None,
) -> Any:
    """Get a value from arbitrarily nested dicts."""
    if dictionary is None:
        return None

    if isinstance(keys, str):
        keys = keys.split(".")
    if not keys:
        return dictionary
    if len(keys) == 1:
        return dictionary.get(keys[0], default)
    return get_in(dictionary.get(keys[0], {}), keys[1:], default=default)

is_during_office_hours ¤

is_during_office_hours(dt: datetime) -> bool

Check whether a datetime is during office hours. 9am-5pm, Mon-Fri.

Parameters:

  • dt (datetime) –

    datetime with TZ info.

Source code in src/firefighter/firefighter/utils.py
def is_during_office_hours(dt: datetime) -> bool:
    """Check whether a datetime is during office hours. 9am-5pm, Mon-Fri.

    Args:
        dt (datetime): datetime with TZ info.
    """
    if (9 <= dt.hour <= 17) and (dt.weekday() < 5):
        return True
    return False

views ¤

CustomDetailView ¤

Bases: DetailView[_MT], Generic[_MT]

A custom detail view that adds the admin edit URL to the context, as admin_edit_url.

get_admin_edit_url cached ¤

get_admin_edit_url(model_class: type[Model], object_pk: Any) -> str | None

Construct the URL name for the admin edit page using the model's app_label and model_name.

Source code in src/firefighter/firefighter/views.py
@cache
def get_admin_edit_url(model_class: type[Model], object_pk: Any) -> str | None:
    """Construct the URL name for the admin edit page using the model's app_label and model_name."""
    app_label: str = model_class._meta.app_label  # noqa: SLF001
    model_name = model_class._meta.model_name  # noqa: SLF001
    url_name: str = f"admin:{app_label}_{model_name}_change"

    try:
        return reverse(url_name, args=[object_pk])
    except NoReverseMatch:
        return None

wsgi ¤

WSGI config for firefighter project.

It exposes the WSGI callable as a module-level variable named application.

For more information on this file, see https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/