ICTU/quality-time

View on GitHub
components/shared_code/src/shared_data_model/parameters.py

Summary

Maintainability
A
1 hr
Test Coverage
"""Data model source parameters."""

from typing import Literal, Self

from pydantic import HttpUrl, model_validator

from .meta.parameter import Parameter, ParameterType
from .meta.unit import Unit


class DateParameter(Parameter):
    """Date parameter."""

    type: ParameterType = ParameterType.DATE


class StringParameter(Parameter):
    """String parameter."""

    type: ParameterType = ParameterType.STRING


class IntegerParameter(Parameter):
    """Integer parameter."""

    type: ParameterType = ParameterType.INTEGER
    default_value: str | list[str] = "0"
    min_value: str = "0"

    @model_validator(mode="after")
    def check_unit(self) -> Self:
        """Check that integer-type parameters have a unit."""
        if self.unit is None:
            msg = f"Parameter {self.name} has no unit"
            raise ValueError(msg)
        return self


class URL(Parameter):
    """URL parameter."""

    name: str = "URL"
    short_name: str = "URL"
    mandatory: bool = True
    validate_on: list[str] = ["username", "password", "private_token"]
    type: ParameterType = ParameterType.URL


class LandingURL(StringParameter):
    """URL to a human readable version of a CSV, XML, or JSON report.

    This is a string parameter because Quality-time doesn't validate these URLs.
    """

    short_name: str = "URL"


class SingleChoiceParameter(Parameter):
    """Single choice parameter."""

    type: ParameterType = ParameterType.SINGLE_CHOICE


class MultipleChoiceParameter(Parameter):
    """Multiple choice parameter."""

    type: ParameterType = ParameterType.MULTIPLE_CHOICE
    default_value: str | list[str] = []


class MultipleChoiceWithAdditionParameter(MultipleChoiceParameter):
    """Multiple choice parameter that allows the user to add additional options."""

    type: ParameterType = ParameterType.MULTIPLE_CHOICE_WITH_ADDITION
    placeholder: str = "none"


class Username(StringParameter):
    """User to be used for authentication."""

    name: str = "Username for basic authentication"
    short_name: str = "username"


class Password(Parameter):
    """Password parameter."""

    name: str = "Password for basic authentication"
    short_name: str = "password"
    type: ParameterType = ParameterType.PASSWORD


class PrivateToken(Password):
    """Private token for authentication."""

    name: str = "Private token"
    short_name: str = "private token"
    validation_path: str = ""  # URL path to use for the validation of tokens


class Days(IntegerParameter):
    """Number of days parameter."""

    unit: Unit = Unit.DAYS
    min_value: str = "1"


class Severities(MultipleChoiceParameter):
    """Security warning severities."""

    name: str = "Severities"
    placeholder: str = "all severities"
    metrics: list[str] = ["security_warnings"]

    @model_validator(mode="after")
    def set_help(self) -> Self:
        """Add a default help string if a help URL was not provided."""
        if not self.help and not self.help_url:
            self.help = "If provided, only count security warnings with the selected severities."
        return self


class TestResult(MultipleChoiceParameter):
    """Test result parameter."""

    name: str = "Test results"
    help: str = (
        "Limit which test results to count. Note: depending on which results are selected, the direction of the "
        "metric may need to be adapted. For example, when counting passed tests, more is better, but when counting "
        "failed tests, fewer is better."
    )
    placeholder: str = "all test results"
    metrics: list[str] = ["tests"]


class Upvotes(IntegerParameter):
    """Minimum number of merge request up-votes parameter."""

    name: str = "Minimum number of upvotes"
    short_name: str = "minimum upvotes"
    help: str = "Only count merge requests with fewer than the minimum number of upvotes."
    unit: Unit = Unit.UPVOTES
    metrics: list[str] = ["merge_requests"]


class Branch(StringParameter):
    """Branch name parameter."""

    name: str = "Branch"
    default_value: str | list[str] = "main"
    metrics: list[str] = ["source_up_to_dateness"]


class Branches(MultipleChoiceWithAdditionParameter):
    """Branches parameter."""

    name: str = "Branches (regular expressions or branch names)"
    short_name: str = "branches"
    placeholder: str = "all branches"
    metrics: list[str] = ["pipeline_duration"]


class BranchesToIgnore(MultipleChoiceWithAdditionParameter):
    """Branches to ignore parameter."""

    name: str = "Branches to ignore (regular expressions or branch names)"
    short_name: str = "branches to ignore"
    metrics: list[str] = ["unmerged_branches"]


class TargetBranchesToInclude(MultipleChoiceWithAdditionParameter):
    """Target branches to include parameter."""

    name: str = "Target branches to include (regular expressions or branch names)"
    short_name: str = "target branches to include"
    placeholder: str = "all target branches"
    metrics: list[str] = ["merge_requests"]


class MergeRequestState(MultipleChoiceParameter):
    """Merge request states parameter."""

    name: str = "Merge request states"
    short_name: str = "states"
    help: str = "Limit which merge request states to count."
    placeholder: str = "all states"
    metrics: list[str] = ["merge_requests"]


class FailureType(MultipleChoiceParameter):
    """Failure type parameter."""

    name: str = "Failure types"
    help: str = "Limit which failure types to count as failed."
    placeholder: str = "all failure types"
    metrics: list[str] = ["failed_jobs"]


def access_parameters(
    metrics: list[str],
    include: dict[str, bool] | None = None,
    source_type: str = "",
    source_type_format: Literal["", "CSV", "HTML", "JSON", "XML"] = "",
    kwargs: dict[str, dict[str, str | bool | HttpUrl | list[str]]] | None = None,
) -> dict[str, Parameter]:
    """Create the access parameters, needed to access the source."""
    include = include or {}
    kwargs = kwargs or {}
    parameters: dict[str, Parameter] = {
        "username": Username(metrics=metrics, **kwargs.get("username", {})),
        "password": Password(metrics=metrics, **kwargs.get("password", {})),
    }
    validate_on = ["username", "password"]
    if include.get("private_token", True):
        private_token_kwargs = kwargs.get("private_token", {})
        parameters["private_token"] = PrivateToken(metrics=metrics, **private_token_kwargs)
        validate_on.append("private_token")
    url_kwargs = kwargs.get("url") or {"name": "URL"}
    if source_type:
        source_type_article = "an" if source_type.startswith("an ") else "a"
        source_type = source_type[len("an ") :] if source_type.startswith("an ") else source_type
        format_phrase = f" in {source_type_format} format" if source_type_format else ""
        url_kwargs["name"] = (
            f"URL to {source_type_article} {source_type}{format_phrase} or to a zip "
            f"with {source_type}s{format_phrase}"
        )
    url_kwargs.setdefault("metrics", metrics)
    parameters["url"] = URL(validate_on=validate_on, **url_kwargs)
    if include.get("landing_url", source_type_format != "HTML"):
        landing_url_name = f"URL to {source_type_article} {source_type} in a human readable format"
        landing_url_help = (
            "If provided, users clicking the source URL will visit this URL instead of the "
            f"{source_type} in {source_type_format} format."
        )
        parameters["landing_url"] = LandingURL(metrics=metrics, name=landing_url_name, help=landing_url_help)
    return parameters