This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new c76e4d98f2c fix(api): advertise only the time grains Prophet can
resolve (#43710)
c76e4d98f2c is described below
commit c76e4d98f2c2abd7a4a598842a02d72347b01eaa
Author: Sepuri Sai Krishna <[email protected]>
AuthorDate: Wed Sep 9 11:09:15 2026 +0530
fix(api): advertise only the time grains Prophet can resolve (#43710)
Co-authored-by: Evan Rusackas <[email protected]>
---
docs/static/resources/openapi.json | 24 ++-----------
superset/charts/schemas.py | 14 +++++++-
tests/unit_tests/charts/test_schemas.py | 61 +++++++++++++++++++++++++++++++++
3 files changed, 76 insertions(+), 23 deletions(-)
diff --git a/docs/static/resources/openapi.json
b/docs/static/resources/openapi.json
index 0ecd2232554..6a6ed1074e9 100644
--- a/docs/static/resources/openapi.json
+++ b/docs/static/resources/openapi.json
@@ -1203,28 +1203,6 @@
},
"time_grain_sqla": {
"description": "To what level of granularity should the temporal
column be aggregated. Supports [ISO
8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) durations.",
- "enum": [
- "PT1S",
- "PT5S",
- "PT30S",
- "PT1M",
- "PT5M",
- "PT10M",
- "PT15M",
- "PT30M",
- "PT1H",
- "PT6H",
- "P1D",
- "P1W",
- "P1M",
- "P3M",
- "P1Y",
- "1969-12-28T00:00:00Z/P1W",
- "1969-12-29T00:00:00Z/P1W",
- "P1W/1970-01-03T00:00:00Z",
- "P1W/1970-01-04T00:00:00Z",
- null
- ],
"example": "P1D",
"nullable": true,
"type": "string"
@@ -1485,12 +1463,14 @@
"PT10M",
"PT15M",
"PT30M",
+ "PT0.5H",
"PT1H",
"PT6H",
"P1D",
"P1W",
"P1M",
"P3M",
+ "P0.25Y",
"P1Y",
"1969-12-28T00:00:00Z/P1W",
"1969-12-29T00:00:00Z/P1W",
diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py
index cd43524f20c..dc97a05f1cf 100644
--- a/superset/charts/schemas.py
+++ b/superset/charts/schemas.py
@@ -83,6 +83,18 @@ def validate_time_grain_sqla(value: Any) -> None:
)(value)
+def get_prophet_time_grain_choices() -> list[str]:
+ """Get the time grains Prophet forecasting can actually resolve.
+
+ Deliberately narrower than :func:`get_time_grain_choices`: ``prophet()``
+ resolves a grain through the static ``PROPHET_TIME_GRAIN_MAP``, so an
+ operator-configured ``TIME_GRAIN_ADDONS`` key has no pandas frequency to
+ resolve to. Advertising one here would document a forecast the API
+ cannot serve.
+ """
+ return list(PROPHET_TIME_GRAIN_MAP)
+
+
# Fallback upper bound for the number of Prophet forecast periods when the
# application config cannot be read (for example, outside of an app context).
DEFAULT_MAX_PROPHET_PERIODS = 10000
@@ -788,7 +800,7 @@ class
ChartDataProphetOptionsSchema(ChartDataPostProcessingOperationOptionsSchem
"[ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations)
durations.",
"example": "P1D",
},
- validate=validate.OneOf(choices=tuple(PROPHET_TIME_GRAIN_MAP.keys())),
+ validate=validate.OneOf(choices=get_prophet_time_grain_choices()),
required=True,
)
periods = fields.Integer(
diff --git a/tests/unit_tests/charts/test_schemas.py
b/tests/unit_tests/charts/test_schemas.py
index 9960b0cc2e2..49fd631ed92 100644
--- a/tests/unit_tests/charts/test_schemas.py
+++ b/tests/unit_tests/charts/test_schemas.py
@@ -21,6 +21,7 @@ from flask import current_app
from jsonschema import validate as validate_json_schema
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
from marshmallow import ValidationError
+from marshmallow.validate import OneOf
from pytest_mock import MockerFixture
from superset.charts.schemas import (
@@ -37,10 +38,12 @@ from superset.charts.schemas import (
ChartPutSchema,
DEFAULT_MAX_PROPHET_PERIODS,
get_max_prophet_periods,
+ get_prophet_time_grain_choices,
get_time_grain_choices,
MAX_VIZ_TYPE_LENGTH,
MAX_VIZ_TYPE_ORDER_LENGTH,
)
+from superset.utils.pandas_postprocessing.utils import PROPHET_TIME_GRAIN_MAP
def test_chart_get_list_schema_accepts_viz_type_display_order() -> None:
@@ -622,3 +625,61 @@ def
test_post_processing_operation_schema_rejects_string_helpers(
schema = ChartDataPostProcessingOperationSchema()
with pytest.raises(ValidationError):
schema.load({"operation": operation, "options": {}})
+
+
+def test_prophet_schema_advertises_only_resolvable_grains(app_context: None)
-> None:
+ """The grains the Prophet field advertises are exactly those prophet()
maps.
+
+ Asserted against the field's own validator rather than the helper, so that
+ re-pointing it at the wider ``get_time_grain_choices()`` fails here.
+ """
+ field = ChartDataProphetOptionsSchema().fields["time_grain"]
+ advertised = {
+ choice
+ for validator in field.validators
+ if isinstance(validator, OneOf)
+ for choice in validator.choices
+ }
+ assert advertised == set(PROPHET_TIME_GRAIN_MAP)
+ assert advertised == set(get_prophet_time_grain_choices())
+
+
+def test_prophet_choices_exclude_an_addon_the_shared_helper_includes(
+ app_context: None,
+) -> None:
+ """A TIME_GRAIN_ADDONS key stays in ``get_time_grain_choices()``, not
Prophet's.
+
+ Asserted on the helpers, which is where the narrowing lives. A field's
+ ``OneOf`` choices are evaluated once, when the schema module is imported,
so
+ the addon set here does not reach the field; the field-level guarantee is
+ covered by ``test_prophet_schema_advertises_only_resolvable_grains``.
+ """
+ original_addons = current_app.config.get("TIME_GRAIN_ADDONS", {})
+
+ try:
+ current_app.config["TIME_GRAIN_ADDONS"] = {"PT7M": "7 minute"}
+
+ # The shared helper still advertises the addon - it is not narrowed.
+ assert "PT7M" in get_time_grain_choices()
+
+ # Prophet does not, because it has no frequency to resolve it to.
+ assert "PT7M" not in get_prophet_time_grain_choices()
+
+ # The field rejects it as well, whatever TIME_GRAIN_ADDONS holds at
+ # import time, because its choices come from the map.
+ with pytest.raises(ValidationError) as exc_info:
+ ChartDataProphetOptionsSchema().load(
+ {"time_grain": "PT7M", "periods": 7, "confidence_interval":
0.8}
+ )
+ assert "time_grain" in exc_info.value.messages
+ finally:
+ current_app.config["TIME_GRAIN_ADDONS"] = original_addons
+
+
[email protected]("grain", ["PT5S", "PT30S", "PT6H", "PT0.5H",
"P0.25Y"])
+def test_prophet_accepts_every_mapped_grain(app_context: None, grain: str) ->
None:
+ """Grains that resolve in the map load through the schema."""
+ result = ChartDataProphetOptionsSchema().load(
+ {"time_grain": grain, "periods": 7, "confidence_interval": 0.8}
+ )
+ assert result["time_grain"] == grain