From e138f1094d92e1ec7995976c7bb1c5e6d2101274 Mon Sep 17 00:00:00 2001 From: Petr Jeske Date: Tue, 17 Feb 2026 16:31:22 +0100 Subject: [PATCH] feat: Introduce AllTimeDateFilter and date filter emptyValueHandling JIRA: LX-2081 risk: low --- ...solute_date_filter_absolute_date_filter.py | 7 + .../model/afm_filters_inner.py | 4 + .../model/all_time_date_filter.py | 281 ++++++++++++++++ ...l_time_date_filter_all_time_date_filter.py | 303 ++++++++++++++++++ .../gooddata_api_client/model/date_filter.py | 7 + .../model/filter_definition.py | 7 + ...lative_date_filter_relative_date_filter.py | 7 + .../gooddata_api_client/models/__init__.py | 2 + .../gooddata-sdk/src/gooddata_sdk/__init__.py | 2 +- .../compute/compute_to_sdk_converter.py | 24 +- .../src/gooddata_sdk/compute/model/filter.py | 98 +++++- .../compute/visualization_to_sdk_converter.py | 6 +- .../src/gooddata_sdk/visualization.py | 26 +- ...ric_and_all_time_date_filter.snapshot.json | 33 ++ ...er_with_empty_value_handling.snapshot.json | 13 + .../all_time_date_filter.snapshot.json | 11 + ...er_with_empty_value_handling.snapshot.json | 14 + .../tests/compute_model/test_compute_model.py | 8 +- .../tests/compute_model/test_date_filters.py | 48 ++- .../compute_model/test_metric_value_filter.py | 2 +- schemas/gooddata-afm-client.json | 86 +++++ schemas/gooddata-api-client.json | 86 +++++ 22 files changed, 1040 insertions(+), 35 deletions(-) create mode 100644 gooddata-api-client/gooddata_api_client/model/all_time_date_filter.py create mode 100644 gooddata-api-client/gooddata_api_client/model/all_time_date_filter_all_time_date_filter.py create mode 100644 packages/gooddata-sdk/tests/compute_model/afm/metric_and_all_time_date_filter.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/date_filters/absolute_date_filter_with_empty_value_handling.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/date_filters/all_time_date_filter.snapshot.json create mode 100644 packages/gooddata-sdk/tests/compute_model/date_filters/relative_date_filter_with_empty_value_handling.snapshot.json diff --git a/gooddata-api-client/gooddata_api_client/model/absolute_date_filter_absolute_date_filter.py b/gooddata-api-client/gooddata_api_client/model/absolute_date_filter_absolute_date_filter.py index 5dbd388ec..e8c58c77c 100644 --- a/gooddata-api-client/gooddata_api_client/model/absolute_date_filter_absolute_date_filter.py +++ b/gooddata-api-client/gooddata_api_client/model/absolute_date_filter_absolute_date_filter.py @@ -60,6 +60,11 @@ class AbsoluteDateFilterAbsoluteDateFilter(ModelNormal): """ allowed_values = { + ('empty_value_handling',): { + 'INCLUDE': "INCLUDE", + 'EXCLUDE': "EXCLUDE", + 'ONLY': "ONLY", + }, } validations = { @@ -99,6 +104,7 @@ def openapi_types(): lazy_import() return { 'dataset': (AfmObjectIdentifierDataset,), # noqa: E501 + 'empty_value_handling': (str,), # noqa: E501 '_from': (str,), # noqa: E501 'to': (str,), # noqa: E501 'apply_on_result': (bool,), # noqa: E501 @@ -112,6 +118,7 @@ def discriminator(): attribute_map = { 'dataset': 'dataset', # noqa: E501 + 'empty_value_handling': 'emptyValueHandling', # noqa: E501 '_from': 'from', # noqa: E501 'to': 'to', # noqa: E501 'apply_on_result': 'applyOnResult', # noqa: E501 diff --git a/gooddata-api-client/gooddata_api_client/model/afm_filters_inner.py b/gooddata-api-client/gooddata_api_client/model/afm_filters_inner.py index b70467c83..77add655b 100644 --- a/gooddata-api-client/gooddata_api_client/model/afm_filters_inner.py +++ b/gooddata-api-client/gooddata_api_client/model/afm_filters_inner.py @@ -32,6 +32,7 @@ def lazy_import(): from gooddata_api_client.model.absolute_date_filter_absolute_date_filter import AbsoluteDateFilterAbsoluteDateFilter + from gooddata_api_client.model.all_time_date_filter_all_time_date_filter import AllTimeDateFilterAllTimeDateFilter from gooddata_api_client.model.abstract_measure_value_filter import AbstractMeasureValueFilter from gooddata_api_client.model.comparison_measure_value_filter_comparison_measure_value_filter import ComparisonMeasureValueFilterComparisonMeasureValueFilter from gooddata_api_client.model.compound_measure_value_filter_compound_measure_value_filter import CompoundMeasureValueFilterCompoundMeasureValueFilter @@ -45,6 +46,7 @@ def lazy_import(): from gooddata_api_client.model.ranking_filter_ranking_filter import RankingFilterRankingFilter from gooddata_api_client.model.relative_date_filter_relative_date_filter import RelativeDateFilterRelativeDateFilter globals()['AbsoluteDateFilterAbsoluteDateFilter'] = AbsoluteDateFilterAbsoluteDateFilter + globals()['AllTimeDateFilterAllTimeDateFilter'] = AllTimeDateFilterAllTimeDateFilter globals()['AbstractMeasureValueFilter'] = AbstractMeasureValueFilter globals()['ComparisonMeasureValueFilterComparisonMeasureValueFilter'] = ComparisonMeasureValueFilterComparisonMeasureValueFilter globals()['CompoundMeasureValueFilterCompoundMeasureValueFilter'] = CompoundMeasureValueFilterCompoundMeasureValueFilter @@ -110,6 +112,7 @@ def openapi_types(): 'compound_measure_value_filter': (CompoundMeasureValueFilterCompoundMeasureValueFilter,), # noqa: E501 'ranking_filter': (RankingFilterRankingFilter,), # noqa: E501 'absolute_date_filter': (AbsoluteDateFilterAbsoluteDateFilter,), # noqa: E501 + 'all_time_date_filter': (AllTimeDateFilterAllTimeDateFilter,), # noqa: E501 'relative_date_filter': (RelativeDateFilterRelativeDateFilter,), # noqa: E501 'negative_attribute_filter': (NegativeAttributeFilterNegativeAttributeFilter,), # noqa: E501 'positive_attribute_filter': (PositiveAttributeFilterPositiveAttributeFilter,), # noqa: E501 @@ -128,6 +131,7 @@ def discriminator(): 'compound_measure_value_filter': 'compoundMeasureValueFilter', # noqa: E501 'ranking_filter': 'rankingFilter', # noqa: E501 'absolute_date_filter': 'absoluteDateFilter', # noqa: E501 + 'all_time_date_filter': 'allTimeDateFilter', # noqa: E501 'relative_date_filter': 'relativeDateFilter', # noqa: E501 'negative_attribute_filter': 'negativeAttributeFilter', # noqa: E501 'positive_attribute_filter': 'positiveAttributeFilter', # noqa: E501 diff --git a/gooddata-api-client/gooddata_api_client/model/all_time_date_filter.py b/gooddata-api-client/gooddata_api_client/model/all_time_date_filter.py new file mode 100644 index 000000000..16c0cf4de --- /dev/null +++ b/gooddata-api-client/gooddata_api_client/model/all_time_date_filter.py @@ -0,0 +1,281 @@ +""" + OpenAPI definition + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501 + + The version of the OpenAPI document: v0 + Contact: support@gooddata.com + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from gooddata_api_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from gooddata_api_client.exceptions import ApiAttributeError + + +def lazy_import(): + from gooddata_api_client.model.all_time_date_filter_all_time_date_filter import AllTimeDateFilterAllTimeDateFilter + + globals()["AllTimeDateFilterAllTimeDateFilter"] = AllTimeDateFilterAllTimeDateFilter + + +class AllTimeDateFilter(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = {} + validations = {} + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + lazy_import() + return (bool, date, datetime, dict, float, int, list, str, none_type) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + lazy_import() + return { + "all_time_date_filter": (AllTimeDateFilterAllTimeDateFilter,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "all_time_date_filter": "allTimeDateFilter", # noqa: E501 + } + + read_only_vars = {} + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, all_time_date_filter, *args, **kwargs): # noqa: E501 + """AllTimeDateFilter - a model defined in OpenAPI + + Args: + all_time_date_filter (AllTimeDateFilterAllTimeDateFilter): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", True) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.all_time_date_filter = all_time_date_filter + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = { + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + } + + @convert_js_args_to_python_args + def __init__(self, all_time_date_filter, *args, **kwargs): # noqa: E501 + """AllTimeDateFilter - a model defined in OpenAPI + + Args: + all_time_date_filter (AllTimeDateFilterAllTimeDateFilter): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.all_time_date_filter = all_time_date_filter + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + f"class with read only attributes." + ) + diff --git a/gooddata-api-client/gooddata_api_client/model/all_time_date_filter_all_time_date_filter.py b/gooddata-api-client/gooddata_api_client/model/all_time_date_filter_all_time_date_filter.py new file mode 100644 index 000000000..71d4220fc --- /dev/null +++ b/gooddata-api-client/gooddata_api_client/model/all_time_date_filter_all_time_date_filter.py @@ -0,0 +1,303 @@ +""" + OpenAPI definition + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501 + + The version of the OpenAPI document: v0 + Contact: support@gooddata.com + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from gooddata_api_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from gooddata_api_client.exceptions import ApiAttributeError + + +def lazy_import(): + from gooddata_api_client.model.afm_object_identifier_dataset import AfmObjectIdentifierDataset + + globals()["AfmObjectIdentifierDataset"] = AfmObjectIdentifierDataset + + +class AllTimeDateFilterAllTimeDateFilter(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + ("empty_value_handling",): { + "INCLUDE": "INCLUDE", + "EXCLUDE": "EXCLUDE", + "ONLY": "ONLY", + }, + ("granularity",): { + "MINUTE": "MINUTE", + "HOUR": "HOUR", + "DAY": "DAY", + "WEEK": "WEEK", + "MONTH": "MONTH", + "QUARTER": "QUARTER", + "YEAR": "YEAR", + "MINUTE_OF_HOUR": "MINUTE_OF_HOUR", + "HOUR_OF_DAY": "HOUR_OF_DAY", + "DAY_OF_WEEK": "DAY_OF_WEEK", + "DAY_OF_MONTH": "DAY_OF_MONTH", + "DAY_OF_QUARTER": "DAY_OF_QUARTER", + "DAY_OF_YEAR": "DAY_OF_YEAR", + "WEEK_OF_YEAR": "WEEK_OF_YEAR", + "MONTH_OF_YEAR": "MONTH_OF_YEAR", + "QUARTER_OF_YEAR": "QUARTER_OF_YEAR", + "FISCAL_MONTH": "FISCAL_MONTH", + "FISCAL_QUARTER": "FISCAL_QUARTER", + "FISCAL_YEAR": "FISCAL_YEAR", + }, + } + + validations = {} + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + lazy_import() + return (bool, date, datetime, dict, float, int, list, str, none_type) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + lazy_import() + return { + "dataset": (AfmObjectIdentifierDataset,), # noqa: E501 + "granularity": (str,), # noqa: E501 + "empty_value_handling": (str,), # noqa: E501 + "apply_on_result": (bool,), # noqa: E501 + "local_identifier": (str,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "dataset": "dataset", # noqa: E501 + "granularity": "granularity", # noqa: E501 + "empty_value_handling": "emptyValueHandling", # noqa: E501 + "apply_on_result": "applyOnResult", # noqa: E501 + "local_identifier": "localIdentifier", # noqa: E501 + } + + read_only_vars = {} + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, dataset, *args, **kwargs): # noqa: E501 + """AllTimeDateFilterAllTimeDateFilter - a model defined in OpenAPI + + Args: + dataset (AfmObjectIdentifierDataset): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + apply_on_result (bool): [optional] # noqa: E501 + empty_value_handling (str): [optional] # noqa: E501 + granularity (str): [optional] # noqa: E501 + local_identifier (str): [optional] # noqa: E501 + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", True) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.dataset = dataset + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = { + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + } + + @convert_js_args_to_python_args + def __init__(self, dataset, *args, **kwargs): # noqa: E501 + """AllTimeDateFilterAllTimeDateFilter - a model defined in OpenAPI + + Args: + dataset (AfmObjectIdentifierDataset): + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + apply_on_result (bool): [optional] # noqa: E501 + empty_value_handling (str): [optional] # noqa: E501 + granularity (str): [optional] # noqa: E501 + local_identifier (str): [optional] # noqa: E501 + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + for arg in args: + if isinstance(arg, dict): + kwargs.update(arg) + else: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.dataset = dataset + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + f"class with read only attributes." + ) + diff --git a/gooddata-api-client/gooddata_api_client/model/date_filter.py b/gooddata-api-client/gooddata_api_client/model/date_filter.py index 3b2e25293..08ea26a7c 100644 --- a/gooddata-api-client/gooddata_api_client/model/date_filter.py +++ b/gooddata-api-client/gooddata_api_client/model/date_filter.py @@ -31,10 +31,14 @@ def lazy_import(): + from gooddata_api_client.model.all_time_date_filter import AllTimeDateFilter + from gooddata_api_client.model.all_time_date_filter_all_time_date_filter import AllTimeDateFilterAllTimeDateFilter from gooddata_api_client.model.absolute_date_filter import AbsoluteDateFilter from gooddata_api_client.model.absolute_date_filter_absolute_date_filter import AbsoluteDateFilterAbsoluteDateFilter from gooddata_api_client.model.relative_date_filter import RelativeDateFilter from gooddata_api_client.model.relative_date_filter_relative_date_filter import RelativeDateFilterRelativeDateFilter + globals()['AllTimeDateFilter'] = AllTimeDateFilter + globals()['AllTimeDateFilterAllTimeDateFilter'] = AllTimeDateFilterAllTimeDateFilter globals()['AbsoluteDateFilter'] = AbsoluteDateFilter globals()['AbsoluteDateFilterAbsoluteDateFilter'] = AbsoluteDateFilterAbsoluteDateFilter globals()['RelativeDateFilter'] = RelativeDateFilter @@ -94,6 +98,7 @@ def openapi_types(): """ lazy_import() return { + 'all_time_date_filter': (AllTimeDateFilterAllTimeDateFilter,), # noqa: E501 'absolute_date_filter': (AbsoluteDateFilterAbsoluteDateFilter,), # noqa: E501 'relative_date_filter': (RelativeDateFilterRelativeDateFilter,), # noqa: E501 } @@ -104,6 +109,7 @@ def discriminator(): attribute_map = { + 'all_time_date_filter': 'allTimeDateFilter', # noqa: E501 'absolute_date_filter': 'absoluteDateFilter', # noqa: E501 'relative_date_filter': 'relativeDateFilter', # noqa: E501 } @@ -325,6 +331,7 @@ def _composed_schemas(): 'allOf': [ ], 'oneOf': [ + AllTimeDateFilter, AbsoluteDateFilter, RelativeDateFilter, ], diff --git a/gooddata-api-client/gooddata_api_client/model/filter_definition.py b/gooddata-api-client/gooddata_api_client/model/filter_definition.py index 06881c566..7a7d2d65a 100644 --- a/gooddata-api-client/gooddata_api_client/model/filter_definition.py +++ b/gooddata-api-client/gooddata_api_client/model/filter_definition.py @@ -31,6 +31,8 @@ def lazy_import(): + from gooddata_api_client.model.all_time_date_filter import AllTimeDateFilter + from gooddata_api_client.model.all_time_date_filter_all_time_date_filter import AllTimeDateFilterAllTimeDateFilter from gooddata_api_client.model.absolute_date_filter import AbsoluteDateFilter from gooddata_api_client.model.absolute_date_filter_absolute_date_filter import AbsoluteDateFilterAbsoluteDateFilter from gooddata_api_client.model.comparison_measure_value_filter import ComparisonMeasureValueFilter @@ -51,6 +53,8 @@ def lazy_import(): from gooddata_api_client.model.ranking_filter_ranking_filter import RankingFilterRankingFilter from gooddata_api_client.model.relative_date_filter import RelativeDateFilter from gooddata_api_client.model.relative_date_filter_relative_date_filter import RelativeDateFilterRelativeDateFilter + globals()['AllTimeDateFilter'] = AllTimeDateFilter + globals()['AllTimeDateFilterAllTimeDateFilter'] = AllTimeDateFilterAllTimeDateFilter globals()['AbsoluteDateFilter'] = AbsoluteDateFilter globals()['AbsoluteDateFilterAbsoluteDateFilter'] = AbsoluteDateFilterAbsoluteDateFilter globals()['ComparisonMeasureValueFilter'] = ComparisonMeasureValueFilter @@ -132,6 +136,7 @@ def openapi_types(): 'range_measure_value_filter': (RangeMeasureValueFilterRangeMeasureValueFilter,), # noqa: E501 'compound_measure_value_filter': (CompoundMeasureValueFilterCompoundMeasureValueFilter,), # noqa: E501 'absolute_date_filter': (AbsoluteDateFilterAbsoluteDateFilter,), # noqa: E501 + 'all_time_date_filter': (AllTimeDateFilterAllTimeDateFilter,), # noqa: E501 'relative_date_filter': (RelativeDateFilterRelativeDateFilter,), # noqa: E501 'negative_attribute_filter': (NegativeAttributeFilterNegativeAttributeFilter,), # noqa: E501 'positive_attribute_filter': (PositiveAttributeFilterPositiveAttributeFilter,), # noqa: E501 @@ -150,6 +155,7 @@ def discriminator(): 'range_measure_value_filter': 'rangeMeasureValueFilter', # noqa: E501 'compound_measure_value_filter': 'compoundMeasureValueFilter', # noqa: E501 'absolute_date_filter': 'absoluteDateFilter', # noqa: E501 + 'all_time_date_filter': 'allTimeDateFilter', # noqa: E501 'relative_date_filter': 'relativeDateFilter', # noqa: E501 'negative_attribute_filter': 'negativeAttributeFilter', # noqa: E501 'positive_attribute_filter': 'positiveAttributeFilter', # noqa: E501 @@ -389,6 +395,7 @@ def _composed_schemas(): 'allOf': [ ], 'oneOf': [ + AllTimeDateFilter, AbsoluteDateFilter, ComparisonMeasureValueFilter, CompoundMeasureValueFilter, diff --git a/gooddata-api-client/gooddata_api_client/model/relative_date_filter_relative_date_filter.py b/gooddata-api-client/gooddata_api_client/model/relative_date_filter_relative_date_filter.py index f15b1a387..0ea3b7dd3 100644 --- a/gooddata-api-client/gooddata_api_client/model/relative_date_filter_relative_date_filter.py +++ b/gooddata-api-client/gooddata_api_client/model/relative_date_filter_relative_date_filter.py @@ -62,6 +62,11 @@ class RelativeDateFilterRelativeDateFilter(ModelNormal): """ allowed_values = { + ('empty_value_handling',): { + 'INCLUDE': "INCLUDE", + 'EXCLUDE': "EXCLUDE", + 'ONLY': "ONLY", + }, ('granularity',): { 'MINUTE': "MINUTE", 'HOUR': "HOUR", @@ -115,6 +120,7 @@ def openapi_types(): '_from': (int,), # noqa: E501 'granularity': (str,), # noqa: E501 'to': (int,), # noqa: E501 + 'empty_value_handling': (str,), # noqa: E501 'apply_on_result': (bool,), # noqa: E501 'bounded_filter': (BoundedFilter,), # noqa: E501 'local_identifier': (str,), # noqa: E501 @@ -130,6 +136,7 @@ def discriminator(): '_from': 'from', # noqa: E501 'granularity': 'granularity', # noqa: E501 'to': 'to', # noqa: E501 + 'empty_value_handling': 'emptyValueHandling', # noqa: E501 'apply_on_result': 'applyOnResult', # noqa: E501 'bounded_filter': 'boundedFilter', # noqa: E501 'local_identifier': 'localIdentifier', # noqa: E501 diff --git a/gooddata-api-client/gooddata_api_client/models/__init__.py b/gooddata-api-client/gooddata_api_client/models/__init__.py index ed1bfc812..6182a310b 100644 --- a/gooddata-api-client/gooddata_api_client/models/__init__.py +++ b/gooddata-api-client/gooddata_api_client/models/__init__.py @@ -59,6 +59,8 @@ from gooddata_api_client.model.aac_workspace_data_filter import AacWorkspaceDataFilter from gooddata_api_client.model.absolute_date_filter import AbsoluteDateFilter from gooddata_api_client.model.absolute_date_filter_absolute_date_filter import AbsoluteDateFilterAbsoluteDateFilter +from gooddata_api_client.model.all_time_date_filter import AllTimeDateFilter +from gooddata_api_client.model.all_time_date_filter_all_time_date_filter import AllTimeDateFilterAllTimeDateFilter from gooddata_api_client.model.abstract_measure_value_filter import AbstractMeasureValueFilter from gooddata_api_client.model.active_object_identification import ActiveObjectIdentification from gooddata_api_client.model.ad_hoc_automation import AdHocAutomation diff --git a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py index 9afad5edc..fe6e2c5af 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/__init__.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/__init__.py @@ -252,7 +252,7 @@ from gooddata_sdk.compute.model.filter import ( AbsoluteDateFilter, AllMetricValueFilter, - AllTimeFilter, + AllTimeDateFilter, AttributeFilter, BoundedFilter, CompoundMetricValueFilter, diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py index 16d6fba8e..8e2c4473d 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/compute_to_sdk_converter.py @@ -5,7 +5,7 @@ from gooddata_sdk.compute.model.base import ObjId from gooddata_sdk.compute.model.filter import ( AbsoluteDateFilter, - AllTimeFilter, + AllTimeDateFilter, BoundedFilter, CompoundMetricValueFilter, Filter, @@ -74,7 +74,11 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: # there is a filter present, but means all time if ("from" not in f) or ("to" not in f): - return AllTimeFilter(ref_extract_obj_id(f["dataset"])) + return AllTimeDateFilter( + dataset=ref_extract_obj_id(f["dataset"]), + granularity=f.get("granularity"), + empty_value_handling=f.get("emptyValueHandling"), + ) # Extract bounded filter if present bounded_filter = None @@ -92,12 +96,26 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: from_shift=f["from"], to_shift=f["to"], bounded_filter=bounded_filter, + empty_value_handling=f.get("emptyValueHandling"), + ) + + if "allTimeDateFilter" in filter_dict: + f = filter_dict["allTimeDateFilter"] + return AllTimeDateFilter( + dataset=ref_extract_obj_id(f["dataset"]), + granularity=f.get("granularity"), + empty_value_handling=f.get("emptyValueHandling"), ) if "absoluteDateFilter" in filter_dict: f = filter_dict["absoluteDateFilter"] - return AbsoluteDateFilter(dataset=ref_extract_obj_id(f["dataset"]), from_date=f["from"], to_date=f["to"]) + return AbsoluteDateFilter( + dataset=ref_extract_obj_id(f["dataset"]), + from_date=f["from"], + to_date=f["to"], + empty_value_handling=f.get("emptyValueHandling"), + ) if "comparisonMeasureValueFilter" in filter_dict: f = filter_dict["comparisonMeasureValueFilter"] diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py index f712caf55..9816034a9 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py @@ -14,6 +14,7 @@ import gooddata_api_client.models as afm_models from gooddata_api_client.model_utils import OpenApiModel from gooddata_api_client.models import AbsoluteDateFilterAbsoluteDateFilter as AbsoluteDateFilterBody +from gooddata_api_client.models import AllTimeDateFilterAllTimeDateFilter as AllTimeDateFilterBody from gooddata_api_client.models import ( ComparisonMeasureValueFilterComparisonMeasureValueFilter as ComparisonMeasureValueFilterBody, ) @@ -75,6 +76,8 @@ RangeOperator: TypeAlias = Literal["BETWEEN", "NOT_BETWEEN"] +EmptyValueHandling: TypeAlias = Literal["INCLUDE", "EXCLUDE", "ONLY"] + def _extract_id_or_local_id(val: Union[ObjId, Attribute, Metric, str]) -> Union[ObjId, str]: if isinstance(val, (str, ObjId)): @@ -199,9 +202,16 @@ def __init__( from_shift: int, to_shift: int, bounded_filter: Optional[BoundedFilter] = None, + empty_value_handling: Optional[EmptyValueHandling] = None, ) -> None: super().__init__() + if empty_value_handling is not None and empty_value_handling not in ("INCLUDE", "EXCLUDE", "ONLY"): + raise ValueError( + f"Invalid relative date filter empty value handling '{empty_value_handling}'. " + "It is expected to be one of: INCLUDE, EXCLUDE, ONLY" + ) + if granularity not in _GRANULARITY: raise ValueError( f"Invalid relative date filter granularity '{granularity}'. It is expected to be one of: {_GRANULARITY}" @@ -212,6 +222,7 @@ def __init__( self._from_shift = from_shift self._to_shift = to_shift self._bounded_filter = bounded_filter + self._empty_value_handling = empty_value_handling @property def dataset(self) -> ObjId: @@ -233,6 +244,10 @@ def to_shift(self) -> int: def bounded_filter(self) -> Optional[BoundedFilter]: return self._bounded_filter + @property + def empty_value_handling(self) -> Optional[EmptyValueHandling]: + return self._empty_value_handling + def is_noop(self) -> bool: return False @@ -248,6 +263,9 @@ def as_api_model(self) -> afm_models.RelativeDateFilter: if self.bounded_filter is not None: body_params["bounded_filter"] = self.bounded_filter.as_api_model() + if self.empty_value_handling is not None: + body_params["empty_value_handling"] = self.empty_value_handling + body = RelativeDateFilterBody(**body_params) return afm_models.RelativeDateFilter(body) @@ -291,40 +309,84 @@ def description(self, labels: dict[str, str], format_locale: Optional[str] = Non return f"{labels.get(self.dataset.id, self.dataset.id)}: {range_str}" -# noinspection PyAbstractClass -class AllTimeFilter(Filter): - """Filter that is semantically equivalent to absent filter. +class AllTimeDateFilter(Filter): + def __init__( + self, + dataset: ObjId, + granularity: Optional[str] = None, + empty_value_handling: Optional[EmptyValueHandling] = None, + ) -> None: + super().__init__() - This filter exists because 'All time filter' retrieved from GoodData.CN - is non-standard as it does not have `from` and `to` fields; - this is also the reason why as_api_model method is not implemented - it - would lead to invalid object. + if empty_value_handling is not None and empty_value_handling not in ("INCLUDE", "EXCLUDE", "ONLY"): + raise ValueError( + f"Invalid all time date filter empty value handling '{empty_value_handling}'. " + "It is expected to be one of: INCLUDE, EXCLUDE, ONLY" + ) - The main feature of this filter is noop. - """ + if granularity is not None and granularity not in _GRANULARITY: + raise ValueError( + f"Invalid all time date filter granularity '{granularity}'. It is expected to be one of: {_GRANULARITY}" + ) - def __init__(self, dataset: ObjId) -> None: - super().__init__() self._dataset = dataset + self._granularity = granularity + self._empty_value_handling = empty_value_handling @property def dataset(self) -> ObjId: return self._dataset + @property + def granularity(self) -> Optional[str]: + return self._granularity + + @property + def empty_value_handling(self) -> Optional[EmptyValueHandling]: + return self._empty_value_handling + def is_noop(self) -> bool: - return True + return self.empty_value_handling is None or self.empty_value_handling == "INCLUDE" + + def as_api_model(self) -> afm_models.AllTimeDateFilter: + body_params: dict[str, Any] = { + "dataset": self.dataset.as_afm_id(), + "_check_type": False, + } + + if self.granularity is not None: + body_params["granularity"] = self.granularity + + if self.empty_value_handling is not None: + body_params["empty_value_handling"] = self.empty_value_handling + + body = AllTimeDateFilterBody(**body_params) + return afm_models.AllTimeDateFilter(body, _check_type=False) def description(self, labels: dict[str, str], format_locale: Optional[str] = None) -> str: return f"{labels.get(self.dataset.id, self.dataset.id)}: All time" class AbsoluteDateFilter(Filter): - def __init__(self, dataset: ObjId, from_date: str, to_date: str) -> None: + def __init__( + self, + dataset: ObjId, + from_date: str, + to_date: str, + empty_value_handling: Optional[EmptyValueHandling] = None, + ) -> None: super().__init__() + if empty_value_handling is not None and empty_value_handling not in ("INCLUDE", "EXCLUDE", "ONLY"): + raise ValueError( + f"Invalid absolute date filter empty value handling '{empty_value_handling}'. " + "It is expected to be one of: INCLUDE, EXCLUDE, ONLY" + ) + self._dataset = dataset self._from_date = from_date self._to_date = to_date + self._empty_value_handling = empty_value_handling @property def dataset(self) -> ObjId: @@ -338,16 +400,24 @@ def from_date(self) -> str: def to_date(self) -> str: return self._to_date + @property + def empty_value_handling(self) -> Optional[EmptyValueHandling]: + return self._empty_value_handling + def is_noop(self) -> bool: return False def as_api_model(self) -> afm_models.AbsoluteDateFilter: - body = AbsoluteDateFilterBody( + body_params: dict[str, Any] = dict( dataset=self.dataset.as_afm_id(), _from=self._from_date, to=self._to_date, _check_type=False, ) + if self.empty_value_handling is not None: + body_params["empty_value_handling"] = self.empty_value_handling + + body = AbsoluteDateFilterBody(**body_params) return afm_models.AbsoluteDateFilter(body) def __eq__(self, other: object) -> bool: diff --git a/packages/gooddata-sdk/src/gooddata_sdk/compute/visualization_to_sdk_converter.py b/packages/gooddata-sdk/src/gooddata_sdk/compute/visualization_to_sdk_converter.py index 624830ed9..66152233d 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/compute/visualization_to_sdk_converter.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/compute/visualization_to_sdk_converter.py @@ -5,7 +5,7 @@ from gooddata_sdk.compute.model.base import ObjId from gooddata_sdk.compute.model.filter import ( AbsoluteDateFilter, - AllTimeFilter, + AllTimeDateFilter, BoundedFilter, Filter, NegativeAttributeFilter, @@ -52,7 +52,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: - NegativeAttributeFilter: When exclude values specified - RelativeDateFilter: When granularity and from/to shifts specified - AbsoluteDateFilter: When from/to dates specified - - AllTimeFilter: When no date range specified + - AllTimeDateFilter: When no date range specified """ using = filter_dict["using"] include = filter_dict.get("include") @@ -84,7 +84,7 @@ def convert_filter(filter_dict: dict[str, Any]) -> Filter: elif _from is not None and _to is not None: return AbsoluteDateFilter(dataset=ObjId(using, "dataset"), from_date=_from, to_date=_to) else: - return AllTimeFilter(dataset=ObjId(using, "dataset")) + return AllTimeDateFilter(dataset=ObjId(using, "dataset")) @staticmethod def convert_metric(metric_dict: dict[str, Any]) -> Metric: diff --git a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py index 20299baf9..b4d369d2f 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/visualization.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/visualization.py @@ -12,7 +12,7 @@ from gooddata_sdk.compute.model.filter import ( AbsoluteDateFilter, AllMetricValueFilter, - AllTimeFilter, + AllTimeDateFilter, BoundedFilter, Filter, MetricValueFilter, @@ -183,7 +183,12 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter: # there is filter present, but uses all time if ("from" not in f) or ("to" not in f): - return AllTimeFilter(ref_extract_obj_id(f["dataSet"])) + granularity = _GRANULARITY_CONVERSION.get(f.get("granularity")) if "granularity" in f else None + return AllTimeDateFilter( + dataset=ref_extract_obj_id(f.get("dataSet") or f.get("dataset")), + granularity=granularity, + empty_value_handling=f.get("emptyValueHandling"), + ) # Extract bounded filter if present bounded_filter = None @@ -201,12 +206,27 @@ def _convert_filter_to_computable(filter_obj: dict[str, Any]) -> Filter: from_shift=f["from"], to_shift=f["to"], bounded_filter=bounded_filter, + empty_value_handling=f.get("emptyValueHandling"), + ) + + elif "allTimeDateFilter" in filter_obj: + f = filter_obj["allTimeDateFilter"] + granularity = _GRANULARITY_CONVERSION.get(f.get("granularity")) if "granularity" in f else None + return AllTimeDateFilter( + dataset=ref_extract_obj_id(f.get("dataSet") or f.get("dataset")), + granularity=granularity, + empty_value_handling=f.get("emptyValueHandling"), ) elif "absoluteDateFilter" in filter_obj: f = filter_obj["absoluteDateFilter"] - return AbsoluteDateFilter(dataset=ref_extract_obj_id(f["dataSet"]), from_date=f["from"], to_date=f["to"]) + return AbsoluteDateFilter( + dataset=ref_extract_obj_id(f["dataSet"]), + from_date=f["from"], + to_date=f["to"], + empty_value_handling=f.get("emptyValueHandling"), + ) elif "measureValueFilter" in filter_obj: f = filter_obj["measureValueFilter"] diff --git a/packages/gooddata-sdk/tests/compute_model/afm/metric_and_all_time_date_filter.snapshot.json b/packages/gooddata-sdk/tests/compute_model/afm/metric_and_all_time_date_filter.snapshot.json new file mode 100644 index 000000000..cd96cc57c --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/afm/metric_and_all_time_date_filter.snapshot.json @@ -0,0 +1,33 @@ +{ + "attributes": [], + "filters": [ + { + "all_time_date_filter": { + "dataset": { + "identifier": { + "id": "dataset.id", + "type": "dataset" + } + }, + "empty_value_handling": "EXCLUDE" + } + } + ], + "measures": [ + { + "definition": { + "measure": { + "compute_ratio": false, + "filters": [], + "item": { + "identifier": { + "id": "metric_id", + "type": "metric" + } + } + } + }, + "local_identifier": "simple_metric_local_id" + } + ] +} \ No newline at end of file diff --git a/packages/gooddata-sdk/tests/compute_model/date_filters/absolute_date_filter_with_empty_value_handling.snapshot.json b/packages/gooddata-sdk/tests/compute_model/date_filters/absolute_date_filter_with_empty_value_handling.snapshot.json new file mode 100644 index 000000000..c92feeaa8 --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/date_filters/absolute_date_filter_with_empty_value_handling.snapshot.json @@ -0,0 +1,13 @@ +{ + "absolute_date_filter": { + "_from": "2021-07-01 18:23", + "dataset": { + "identifier": { + "id": "dataset.id", + "type": "dataset" + } + }, + "empty_value_handling": "ONLY", + "to": "2021-07-16 18:23" + } +} \ No newline at end of file diff --git a/packages/gooddata-sdk/tests/compute_model/date_filters/all_time_date_filter.snapshot.json b/packages/gooddata-sdk/tests/compute_model/date_filters/all_time_date_filter.snapshot.json new file mode 100644 index 000000000..020df5ce0 --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/date_filters/all_time_date_filter.snapshot.json @@ -0,0 +1,11 @@ +{ + "all_time_date_filter": { + "dataset": { + "identifier": { + "id": "dataset.id", + "type": "dataset" + } + }, + "empty_value_handling": "EXCLUDE" + } +} \ No newline at end of file diff --git a/packages/gooddata-sdk/tests/compute_model/date_filters/relative_date_filter_with_empty_value_handling.snapshot.json b/packages/gooddata-sdk/tests/compute_model/date_filters/relative_date_filter_with_empty_value_handling.snapshot.json new file mode 100644 index 000000000..711f9a0ec --- /dev/null +++ b/packages/gooddata-sdk/tests/compute_model/date_filters/relative_date_filter_with_empty_value_handling.snapshot.json @@ -0,0 +1,14 @@ +{ + "relative_date_filter": { + "_from": -10, + "dataset": { + "identifier": { + "id": "dataset.id", + "type": "dataset" + } + }, + "empty_value_handling": "ONLY", + "granularity": "DAY", + "to": -1 + } +} \ No newline at end of file diff --git a/packages/gooddata-sdk/tests/compute_model/test_compute_model.py b/packages/gooddata-sdk/tests/compute_model/test_compute_model.py index 7a89f4cb7..f747af146 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_compute_model.py +++ b/packages/gooddata-sdk/tests/compute_model/test_compute_model.py @@ -9,7 +9,7 @@ from gooddata_sdk.compute.model.attribute import Attribute from gooddata_sdk.compute.model.base import Filter, ObjId from gooddata_sdk.compute.model.execution import compute_model_to_api_model -from gooddata_sdk.compute.model.filter import AbsoluteDateFilter, PositiveAttributeFilter +from gooddata_sdk.compute.model.filter import AbsoluteDateFilter, AllTimeDateFilter, PositiveAttributeFilter from gooddata_sdk.compute.model.metric import ( Metric, PopDate, @@ -49,6 +49,11 @@ def _scenario_to_snapshot_name(scenario: str): to_date="2021-07-16 18:23", ) +_all_time_date_filter = AllTimeDateFilter( + dataset=ObjId(type="dataset", id="dataset.id"), + empty_value_handling="EXCLUDE", +) + test_inputs = [ [ "multiple attributes and metrics and filters", @@ -60,6 +65,7 @@ def _scenario_to_snapshot_name(scenario: str): ["attribute and filter ", [_attribute], None, [_positive_filter]], ["metric only ", None, [_simple_metric], None], ["metric and filter ", None, [_simple_metric], [_positive_filter]], + ["metric and all time date filter", None, [_simple_metric], [_all_time_date_filter]], [ "attribute and metric and filter ", [_attribute], diff --git a/packages/gooddata-sdk/tests/compute_model/test_date_filters.py b/packages/gooddata-sdk/tests/compute_model/test_date_filters.py index 58823b80d..7d69f92a3 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_date_filters.py +++ b/packages/gooddata-sdk/tests/compute_model/test_date_filters.py @@ -6,7 +6,7 @@ from importlib.util import find_spec import pytest -from gooddata_sdk import AbsoluteDateFilter, AllTimeFilter, ObjId, RelativeDateFilter +from gooddata_sdk import AbsoluteDateFilter, AllTimeDateFilter, ObjId, RelativeDateFilter _current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -32,6 +32,18 @@ def _scenario_to_snapshot_name(scenario: str): "cs-CZ": "DataSet ID: 1. 7. 2021 - 16. 7. 2021", }, ], + [ + "absolute date filter with empty value handling", + AbsoluteDateFilter( + dataset=ObjId(type="dataset", id="dataset.id"), + from_date="2021-07-01 18:23", + to_date="2021-07-16 18:23", + empty_value_handling="ONLY", + ), + { + "default": "DataSet ID: 7/1/2021 - 7/16/2021", + }, + ], [ "relative date filter", RelativeDateFilter( @@ -44,6 +56,29 @@ def _scenario_to_snapshot_name(scenario: str): "default": "DataSet ID: From 10 days to 1 day ago", }, ], + [ + "relative date filter with empty value handling", + RelativeDateFilter( + dataset=ObjId(type="dataset", id="dataset.id"), + granularity="DAY", + from_shift=-10, + to_shift=-1, + empty_value_handling="ONLY", + ), + { + "default": "DataSet ID: From 10 days to 1 day ago", + }, + ], + [ + "all time date filter", + AllTimeDateFilter( + dataset=ObjId(type="dataset", id="dataset.id"), + empty_value_handling="EXCLUDE", + ), + { + "default": "DataSet ID: All time", + }, + ], ] @@ -71,11 +106,6 @@ def test_date_filters_description(scenario, filter, descriptions): pytest.skip("ICU library not found") -def test_cannot_create_api_model_from_all_time_filter(): - """As All time filter from GoodData.CN does not contain from and to fields, - we are not sure how to make valid model from it. We prefer to fail, until - we decide what to do with this situation. - """ - with pytest.raises(NotImplementedError): - f = AllTimeFilter(dataset=ObjId(type="dataset", id="dataset.id")) - f.as_api_model() +def test_all_time_date_filter_is_noop_by_default(): + f = AllTimeDateFilter(dataset=ObjId(type="dataset", id="dataset.id")) + assert f.is_noop() diff --git a/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py b/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py index 6ecebdcd3..24e10afab 100644 --- a/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py +++ b/packages/gooddata-sdk/tests/compute_model/test_metric_value_filter.py @@ -86,7 +86,7 @@ def test_all_metric_value_filter_description(): def test_cannot_create_api_model_from_all_metric_value_filter(): """ - Analogy to AllTimeFilter test_cannot_create_api_model_from_all_time_filter() in test_date_filters.py + Analogy to AllTimeDateFilter noop behavior test in test_date_filters.py """ with pytest.raises(NotImplementedError): f = AllMetricValueFilter(metric="local_id1") diff --git a/schemas/gooddata-afm-client.json b/schemas/gooddata-afm-client.json index 6476682c1..31dab5da5 100644 --- a/schemas/gooddata-afm-client.json +++ b/schemas/gooddata-afm-client.json @@ -51,6 +51,16 @@ "dataset": { "$ref": "#/components/schemas/AfmObjectIdentifierDataset" }, + "emptyValueHandling": { + "default": "EXCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE includes empty dates in addition to the date range restriction, EXCLUDE removes rows with empty dates (default), ONLY keeps only rows with empty dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, "from": { "example": "2020-07-01 18:23", "pattern": "^\\d{4}-\\d{1,2}-\\d{1,2}( \\d{1,2}:\\d{1,2})?$", @@ -424,6 +434,69 @@ ], "type": "object" }, + "AllTimeDateFilter": { + "description": "An all-time date filter that does not restrict by date range. Controls how rows with empty (null/missing) date values are handled.", + "properties": { + "allTimeDateFilter": { + "properties": { + "applyOnResult": { + "type": "boolean" + }, + "dataset": { + "$ref": "#/components/schemas/AfmObjectIdentifierDataset" + }, + "emptyValueHandling": { + "default": "INCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE means no filtering effect (default), EXCLUDE removes rows with null dates, ONLY keeps only rows with null dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, + "granularity": { + "default": "DAY", + "description": "Date granularity used to resolve the date attribute label for null value checks. Defaults to DAY if not specified.", + "enum": [ + "MINUTE", + "HOUR", + "DAY", + "WEEK", + "MONTH", + "QUARTER", + "YEAR", + "MINUTE_OF_HOUR", + "HOUR_OF_DAY", + "DAY_OF_WEEK", + "DAY_OF_MONTH", + "DAY_OF_QUARTER", + "DAY_OF_YEAR", + "WEEK_OF_YEAR", + "MONTH_OF_YEAR", + "QUARTER_OF_YEAR", + "FISCAL_MONTH", + "FISCAL_QUARTER", + "FISCAL_YEAR" + ], + "example": "DAY", + "type": "string" + }, + "localIdentifier": { + "type": "string" + } + }, + "required": [ + "dataset" + ], + "type": "object" + } + }, + "required": [ + "allTimeDateFilter" + ], + "type": "object" + }, "AllowedRelationshipType": { "description": "Allowed relationship type combination.", "properties": { @@ -2449,6 +2522,9 @@ { "$ref": "#/components/schemas/AbsoluteDateFilter" }, + { + "$ref": "#/components/schemas/AllTimeDateFilter" + }, { "$ref": "#/components/schemas/RelativeDateFilter" }, @@ -3864,6 +3940,16 @@ "dataset": { "$ref": "#/components/schemas/AfmObjectIdentifierDataset" }, + "emptyValueHandling": { + "default": "EXCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE includes empty dates in addition to the date range restriction, EXCLUDE removes rows with empty dates (default), ONLY keeps only rows with empty dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, "from": { "description": "Start of the filtering interval. Specified by number of periods (with respect to given granularity). Typically negative (historical time interval like -2 for '2 days/weeks, ... ago').", "example": -6, diff --git a/schemas/gooddata-api-client.json b/schemas/gooddata-api-client.json index 865f896a4..a0c3f679e 100644 --- a/schemas/gooddata-api-client.json +++ b/schemas/gooddata-api-client.json @@ -4470,6 +4470,16 @@ "dataset": { "$ref": "#/components/schemas/AfmObjectIdentifierDataset" }, + "emptyValueHandling": { + "default": "EXCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE includes empty dates in addition to the date range restriction, EXCLUDE removes rows with empty dates (default), ONLY keeps only rows with empty dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, "from": { "example": "2020-07-01 18:23", "pattern": "^\\d{4}-\\d{1,2}-\\d{1,2}( \\d{1,2}:\\d{1,2})?$", @@ -4947,6 +4957,69 @@ ], "type": "object" }, + "AllTimeDateFilter": { + "description": "An all-time date filter that does not restrict by date range. Controls how rows with empty (null/missing) date values are handled.", + "properties": { + "allTimeDateFilter": { + "properties": { + "applyOnResult": { + "type": "boolean" + }, + "dataset": { + "$ref": "#/components/schemas/AfmObjectIdentifierDataset" + }, + "emptyValueHandling": { + "default": "INCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE means no filtering effect (default), EXCLUDE removes rows with null dates, ONLY keeps only rows with null dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, + "granularity": { + "default": "DAY", + "description": "Date granularity used to resolve the date attribute label for null value checks. Defaults to DAY if not specified.", + "enum": [ + "MINUTE", + "HOUR", + "DAY", + "WEEK", + "MONTH", + "QUARTER", + "YEAR", + "MINUTE_OF_HOUR", + "HOUR_OF_DAY", + "DAY_OF_WEEK", + "DAY_OF_MONTH", + "DAY_OF_QUARTER", + "DAY_OF_YEAR", + "WEEK_OF_YEAR", + "MONTH_OF_YEAR", + "QUARTER_OF_YEAR", + "FISCAL_MONTH", + "FISCAL_QUARTER", + "FISCAL_YEAR" + ], + "example": "DAY", + "type": "string" + }, + "localIdentifier": { + "type": "string" + } + }, + "required": [ + "dataset" + ], + "type": "object" + } + }, + "required": [ + "allTimeDateFilter" + ], + "type": "object" + }, "AlertAfm": { "properties": { "attributes": { @@ -12476,6 +12549,9 @@ { "$ref": "#/components/schemas/AbsoluteDateFilter" }, + { + "$ref": "#/components/schemas/AllTimeDateFilter" + }, { "$ref": "#/components/schemas/RelativeDateFilter" }, @@ -31603,6 +31679,16 @@ "dataset": { "$ref": "#/components/schemas/AfmObjectIdentifierDataset" }, + "emptyValueHandling": { + "default": "EXCLUDE", + "description": "Specifies how rows with empty (null/missing) date values should be handled. INCLUDE includes empty dates in addition to the date range restriction, EXCLUDE removes rows with empty dates (default), ONLY keeps only rows with empty dates.", + "enum": [ + "INCLUDE", + "EXCLUDE", + "ONLY" + ], + "type": "string" + }, "from": { "description": "Start of the filtering interval. Specified by number of periods (with respect to given granularity). Typically negative (historical time interval like -2 for '2 days/weeks, ... ago').", "example": -6,