| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| from typing import Any, Dict, Union |
| |
| from croniter import croniter |
| from flask_babel import gettext as _ |
| from marshmallow import fields, Schema, validate, validates_schema |
| from marshmallow.validate import Length, Range, ValidationError |
| |
| from superset.models.reports import ( |
| ReportRecipientType, |
| ReportScheduleType, |
| ReportScheduleValidatorType, |
| ) |
| |
| openapi_spec_methods_override = { |
| "get": {"get": {"description": "Get a report schedule"}}, |
| "get_list": { |
| "get": { |
| "description": "Get a list of report schedules, use Rison or JSON " |
| "query parameters for filtering, sorting," |
| " pagination and for selecting specific" |
| " columns and metadata.", |
| } |
| }, |
| "post": {"post": {"description": "Create a report schedule"}}, |
| "put": {"put": {"description": "Update a report schedule"}}, |
| "delete": {"delete": {"description": "Delete a report schedule"}}, |
| } |
| |
| get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}} |
| |
| type_description = "The report schedule type" |
| name_description = "The report schedule name." |
| # :) |
| description_description = "Use a nice description to give context to this Alert/Report" |
| context_markdown_description = "Markdown description" |
| crontab_description = ( |
| "A CRON expression." |
| "[Crontab Guru](https://crontab.guru/) is " |
| "a helpful resource that can help you craft a CRON expression." |
| ) |
| sql_description = ( |
| "A SQL statement that defines whether the alert should get triggered or " |
| "not. The query is expected to return either NULL or a number value." |
| ) |
| owners_description = ( |
| "Owner are users ids allowed to delete or change this report. " |
| "If left empty you will be one of the owners of the report." |
| ) |
| validator_type_description = ( |
| "Determines when to trigger alert based off value from alert query. " |
| "Alerts will be triggered with these validator types:\n" |
| "- Not Null - When the return value is Not NULL, Empty, or 0\n" |
| "- Operator - When `sql_return_value comparison_operator threshold`" |
| " is True e.g. `50 <= 75`<br>Supports the comparison operators <, <=, " |
| ">, >=, ==, and !=" |
| ) |
| validator_config_json_op_description = ( |
| "The operation to compare with a threshold to apply to the SQL output\n" |
| ) |
| log_retention_description = "How long to keep the logs around for this report (in days)" |
| grace_period_description = ( |
| "Once an alert is triggered, how long, in seconds, before " |
| "Superset nags you again. (in seconds)" |
| ) |
| working_timeout_description = ( |
| "If an alert is staled at a working state, how long until it's state is reseted to" |
| " error" |
| ) |
| |
| |
| def validate_crontab(value: Union[bytes, bytearray, str]) -> None: |
| if not croniter.is_valid(str(value)): |
| raise ValidationError("Cron expression is not valid") |
| |
| |
| class ValidatorConfigJSONSchema(Schema): |
| op = fields.String( # pylint: disable=invalid-name |
| description=validator_config_json_op_description, |
| validate=validate.OneOf(choices=["<", "<=", ">", ">=", "==", "!="]), |
| ) |
| threshold = fields.Integer() |
| |
| |
| class ReportRecipientConfigJSONSchema(Schema): |
| # TODO if email check validity |
| target = fields.String() |
| |
| |
| class ReportRecipientSchema(Schema): |
| type = fields.String( |
| description="The recipient type, check spec for valid options", |
| allow_none=False, |
| required=True, |
| validate=validate.OneOf( |
| choices=tuple(key.value for key in ReportRecipientType) |
| ), |
| ) |
| recipient_config_json = fields.Nested(ReportRecipientConfigJSONSchema) |
| |
| |
| class ReportSchedulePostSchema(Schema): |
| type = fields.String( |
| description=type_description, |
| allow_none=False, |
| required=True, |
| validate=validate.OneOf(choices=tuple(key.value for key in ReportScheduleType)), |
| ) |
| name = fields.String( |
| description=name_description, |
| allow_none=False, |
| required=True, |
| validate=[Length(1, 150)], |
| example="Daily dashboard email", |
| ) |
| description = fields.String( |
| description=description_description, |
| allow_none=True, |
| required=False, |
| example="Daily sales dashboard to marketing", |
| ) |
| context_markdown = fields.String( |
| description=context_markdown_description, allow_none=True, required=False |
| ) |
| active = fields.Boolean() |
| crontab = fields.String( |
| description=crontab_description, |
| validate=[validate_crontab, Length(1, 1000)], |
| example="*/5 * * * *", |
| allow_none=False, |
| required=True, |
| ) |
| sql = fields.String( |
| description=sql_description, example="SELECT value FROM time_series_table" |
| ) |
| chart = fields.Integer(required=False) |
| dashboard = fields.Integer(required=False) |
| database = fields.Integer(required=False) |
| owners = fields.List(fields.Integer(description=owners_description)) |
| validator_type = fields.String( |
| description=validator_type_description, |
| validate=validate.OneOf( |
| choices=tuple(key.value for key in ReportScheduleValidatorType) |
| ), |
| ) |
| validator_config_json = fields.Nested(ValidatorConfigJSONSchema) |
| log_retention = fields.Integer( |
| description=log_retention_description, |
| example=90, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| grace_period = fields.Integer( |
| description=grace_period_description, |
| example=60 * 60 * 4, |
| default=60 * 60 * 4, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| working_timeout = fields.Integer( |
| description=working_timeout_description, |
| example=60 * 60 * 1, |
| default=60 * 60 * 1, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| |
| recipients = fields.List(fields.Nested(ReportRecipientSchema)) |
| |
| @validates_schema |
| def validate_report_references( # pylint: disable=unused-argument,no-self-use |
| self, data: Dict[str, Any], **kwargs: Any |
| ) -> None: |
| if data["type"] == ReportScheduleType.REPORT: |
| if "database" in data: |
| raise ValidationError( |
| {"database": ["Database reference is not allowed on a report"]} |
| ) |
| |
| |
| class ReportSchedulePutSchema(Schema): |
| type = fields.String( |
| description=type_description, |
| required=False, |
| validate=validate.OneOf(choices=tuple(key.value for key in ReportScheduleType)), |
| ) |
| name = fields.String( |
| description=name_description, required=False, validate=[Length(1, 150)] |
| ) |
| description = fields.String( |
| description=description_description, |
| allow_none=True, |
| required=False, |
| example="Daily sales dashboard to marketing", |
| ) |
| context_markdown = fields.String( |
| description=context_markdown_description, allow_none=True, required=False |
| ) |
| active = fields.Boolean(required=False) |
| crontab = fields.String( |
| description=crontab_description, |
| validate=[validate_crontab, Length(1, 1000)], |
| required=False, |
| ) |
| sql = fields.String( |
| description=sql_description, |
| example="SELECT value FROM time_series_table", |
| required=False, |
| allow_none=True, |
| ) |
| chart = fields.Integer(required=False) |
| dashboard = fields.Integer(required=False) |
| database = fields.Integer(required=False) |
| owners = fields.List(fields.Integer(description=owners_description), required=False) |
| validator_type = fields.String( |
| description=validator_type_description, |
| validate=validate.OneOf( |
| choices=tuple(key.value for key in ReportScheduleValidatorType) |
| ), |
| allow_none=True, |
| required=False, |
| ) |
| validator_config_json = fields.Nested(ValidatorConfigJSONSchema, required=False) |
| log_retention = fields.Integer( |
| description=log_retention_description, |
| example=90, |
| required=False, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| grace_period = fields.Integer( |
| description=grace_period_description, |
| example=60 * 60 * 4, |
| required=False, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| working_timeout = fields.Integer( |
| description=working_timeout_description, |
| example=60 * 60 * 1, |
| allow_none=True, |
| required=False, |
| validate=[Range(min=1, error=_("Value must be greater than 0"))], |
| ) |
| recipients = fields.List(fields.Nested(ReportRecipientSchema), required=False) |