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

Reply via email to