From 4091ee8ca466ec725adeb75534eebe4d7d443327 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Tue, 14 May 2024 09:44:31 +0300 Subject: [PATCH 01/10] Timeseries insertion filters for close samples Support timeseries insertion filters for samples that are close to each other in time and value. --- redis/commands/timeseries/commands.py | 755 +++++++++++++++----------- tests/test_timeseries.py | 82 ++- 2 files changed, 508 insertions(+), 329 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index 208ddfb09f..c24f6e87a6 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -33,44 +33,64 @@ def create( labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, duplicate_policy: Optional[str] = None, + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, ): """ - Create a new time-series. + Creates a new time-series. - Args: + For more information, see the Redis command details: + https://redis.io/commands/ts.create/ - key: - time-series key - retention_msecs: - Maximum age for samples compared to highest reported timestamp (in milliseconds). - If None or 0 is passed then the series is not trimmed at all. - uncompressed: - Changes data storage from compressed (by default) to uncompressed - labels: - Set of label-value pairs that represent metadata labels of the key. - chunk_size: - Memory size, in bytes, allocated for each data chunk. - Must be a multiple of 8 in the range [128 .. 1048576]. - duplicate_policy: - Policy for handling multiple samples with identical timestamps. - Can be one of: - - 'block': an error will occur for any out of order sample. - - 'first': ignore the new value. - - 'last': override with latest value. - - 'min': only override if the value is lower than the existing value. - - 'max': only override if the value is higher than the existing value. - - 'sum': If a previous sample exists, add the new sample to it so that \ - the updated value is equal to (previous + new). If no previous sample \ - exists, set the updated value equal to the new value. - - For more information: https://redis.io/commands/ts.create/ - """ # noqa + Args: + key: + The time-series key. + retention_msecs: + Maximum age for samples, compared to the highest reported timestamp in + milliseconds. If None or 0 is passed, the series is not trimmed at all. + uncompressed: + Changes data storage from compressed (default) to uncompressed. + labels: + A dictionary of label-value pairs that represent metadata labels of the + key. + chunk_size: + Memory size, in bytes, allocated for each data chunk. Must be a multiple + of 8 in the range [128..1048576]. + duplicate_policy: + Policy for handling multiple samples with identical timestamps. Can be + one of: + - 'block': An error will occur for any out of order sample. + - 'first': Ignore the new value. + - 'last': Override with the latest value. + - 'min': Only override if the value is lower than the existing + value. + - 'max': Only override if the value is higher than the existing + value. + - 'sum': If a previous sample exists, add the new sample to it so + that the updated value is equal to (previous + new). If no + previous sample exists, set the updated value equal to the new + value. + ignore_max_time_diff: + A non-negative integer value, in milliseconds, that sets an ignore + threshold for added timestamps. If the difference between the last + timestamp and the new timestamp is lower than this threshold, the new + entry is ignored. Only applicable if `duplicate_policy` is set to + `last`, and if `ignore_max_val_diff` is also set. Available since + RedisTimeSeries version 1.12.0. + ignore_max_val_diff: + A non-negative floating point value, that sets an ignore threshold for + added values. If the difference between the last value and the new value + is lower than this threshold, the new entry is ignored. Only applicable + if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is + also set. Available since RedisTimeSeries version 1.12.0. + """ params = [key] self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, CREATE_CMD, duplicate_policy) self._append_labels(params, labels) + self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) return self.execute_command(CREATE_CMD, *params) @@ -81,42 +101,62 @@ def alter( labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, duplicate_policy: Optional[str] = None, + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, ): """ Update the retention, chunk size, duplicate policy, and labels of an existing time series. - Args: + For more information, see the Redis command details: + https://redis.io/commands/ts.alter/ - key: - time-series key - retention_msecs: - Maximum retention period, compared to maximal existing timestamp (in milliseconds). - If None or 0 is passed then the series is not trimmed at all. - labels: - Set of label-value pairs that represent metadata labels of the key. - chunk_size: - Memory size, in bytes, allocated for each data chunk. - Must be a multiple of 8 in the range [128 .. 1048576]. - duplicate_policy: - Policy for handling multiple samples with identical timestamps. - Can be one of: - - 'block': an error will occur for any out of order sample. - - 'first': ignore the new value. - - 'last': override with latest value. - - 'min': only override if the value is lower than the existing value. - - 'max': only override if the value is higher than the existing value. - - 'sum': If a previous sample exists, add the new sample to it so that \ - the updated value is equal to (previous + new). If no previous sample \ - exists, set the updated value equal to the new value. - - For more information: https://redis.io/commands/ts.alter/ - """ # noqa + Args: + key: + The time-series key. + retention_msecs: + Maximum age for samples, compared to the highest reported timestamp in + milliseconds. If None or 0 is passed, the series is not trimmed at all. + labels: + A dictionary of label-value pairs that represent metadata labels of the + key. + chunk_size: + Memory size, in bytes, allocated for each data chunk. Must be a multiple + of 8 in the range [128..1048576]. + duplicate_policy: + Policy for handling multiple samples with identical timestamps. Can be + one of: + - 'block': An error will occur for any out of order sample. + - 'first': Ignore the new value. + - 'last': Override with the latest value. + - 'min': Only override if the value is lower than the existing + value. + - 'max': Only override if the value is higher than the existing + value. + - 'sum': If a previous sample exists, add the new sample to it so + that the updated value is equal to (previous + new). If no + previous sample exists, set the updated value equal to the new + value. + ignore_max_time_diff: + A non-negative integer value, in milliseconds, that sets an ignore + threshold for added timestamps. If the difference between the last + timestamp and the new timestamp is lower than this threshold, the new + entry is ignored. Only applicable if `duplicate_policy` is set to + `last`, and if `ignore_max_val_diff` is also set. Available since + RedisTimeSeries version 1.12.0. + ignore_max_val_diff: + A non-negative floating point value, that sets an ignore threshold for + added values. If the difference between the last value and the new value + is lower than this threshold, the new entry is ignored. Only applicable + if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is + also set. Available since RedisTimeSeries version 1.12.0. + """ params = [key] self._append_retention(params, retention_msecs) self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, ALTER_CMD, duplicate_policy) self._append_labels(params, labels) + self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) return self.execute_command(ALTER_CMD, *params) @@ -130,60 +170,81 @@ def add( labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, duplicate_policy: Optional[str] = None, + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, ): """ Append (or create and append) a new sample to a time series. - Args: + For more information, see the Redis command details: + https://redis.io/commands/ts.add/ - key: - time-series key - timestamp: - Timestamp of the sample. * can be used for automatic timestamp (using the system clock). - value: - Numeric data value of the sample - retention_msecs: - Maximum retention period, compared to maximal existing timestamp (in milliseconds). - If None or 0 is passed then the series is not trimmed at all. - uncompressed: - Changes data storage from compressed (by default) to uncompressed - labels: - Set of label-value pairs that represent metadata labels of the key. - chunk_size: - Memory size, in bytes, allocated for each data chunk. - Must be a multiple of 8 in the range [128 .. 1048576]. - duplicate_policy: - Policy for handling multiple samples with identical timestamps. - Can be one of: - - 'block': an error will occur for any out of order sample. - - 'first': ignore the new value. - - 'last': override with latest value. - - 'min': only override if the value is lower than the existing value. - - 'max': only override if the value is higher than the existing value. - - 'sum': If a previous sample exists, add the new sample to it so that \ - the updated value is equal to (previous + new). If no previous sample \ - exists, set the updated value equal to the new value. - - For more information: https://redis.io/commands/ts.add/ - """ # noqa + Args: + key: + The time-series key. + timestamp: + Timestamp of the sample. `*` can be used for automatic timestamp (using + the system clock). + value: + Numeric data value of the sample. + retention_msecs: + Maximum age for samples, compared to the highest reported timestamp in + milliseconds. If None or 0 is passed, the series is not trimmed at all. + uncompressed: + Changes data storage from compressed (default) to uncompressed. + labels: + A dictionary of label-value pairs that represent metadata labels of the + key. + chunk_size: + Memory size, in bytes, allocated for each data chunk. Must be a multiple + of 8 in the range [128..1048576]. + duplicate_policy: + Policy for handling multiple samples with identical timestamps. Can be + one of: + - 'block': An error will occur for any out of order sample. + - 'first': Ignore the new value. + - 'last': Override with the latest value. + - 'min': Only override if the value is lower than the existing + value. + - 'max': Only override if the value is higher than the existing + value. + - 'sum': If a previous sample exists, add the new sample to it so + that the updated value is equal to (previous + new). If no + previous sample exists, set the updated value equal to the new + value. + ignore_max_time_diff: + A non-negative integer value, in milliseconds, that sets an ignore + threshold for added timestamps. If the difference between the last + timestamp and the new timestamp is lower than this threshold, the new + entry is ignored. Only applicable if `duplicate_policy` is set to + `last`, and if `ignore_max_val_diff` is also set. Available since + RedisTimeSeries version 1.12.0. + ignore_max_val_diff: + A non-negative floating point value, that sets an ignore threshold for + added values. If the difference between the last value and the new value + is lower than this threshold, the new entry is ignored. Only applicable + if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is + also set. Available since RedisTimeSeries version 1.12.0. + """ params = [key, timestamp, value] self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, ADD_CMD, duplicate_policy) self._append_labels(params, labels) + self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) return self.execute_command(ADD_CMD, *params) def madd(self, ktv_tuples: List[Tuple[KeyT, Union[int, str], Number]]): """ - Append (or create and append) a new `value` to series - `key` with `timestamp`. + Append (or create and append) a new `value` to series `key` with `timestamp`. Expects a list of `tuples` as (`key`,`timestamp`, `value`). + Return value is an array with timestamps of insertions. For more information: https://redis.io/commands/ts.madd/ - """ # noqa + """ params = [] for ktv in ktv_tuples: params.extend(ktv) @@ -204,26 +265,26 @@ def incrby( Increment (or create an time-series and increment) the latest sample's of a series. This command can be used as a counter or gauge that automatically gets history as a time series. - Args: - - key: - time-series key - value: - Numeric data value of the sample - timestamp: - Timestamp of the sample. * can be used for automatic timestamp (using the system clock). - retention_msecs: - Maximum age for samples compared to last event time (in milliseconds). - If None or 0 is passed then the series is not trimmed at all. - uncompressed: - Changes data storage from compressed (by default) to uncompressed - labels: - Set of label-value pairs that represent metadata labels of the key. - chunk_size: - Memory size, in bytes, allocated for each data chunk. - For more information: https://redis.io/commands/ts.incrby/ - """ # noqa + + Args: + key: + The time-series key. + value: + Numeric data value of the sample. + timestamp: + Timestamp of the sample. `*` can be used for automatic timestamp (using + the system clock). + retention_msecs: + Maximum age for samples compared to last event time (in milliseconds). + If `None` or `0` is passed then the series is not trimmed at all. + uncompressed: + Changes data storage from compressed (by default) to uncompressed. + labels: + Set of label-value pairs that represent metadata labels of the key. + chunk_size: + Memory size, in bytes, allocated for each data chunk. + """ params = [key, value] self._append_timestamp(params, timestamp) self._append_retention(params, retention_msecs) @@ -247,26 +308,26 @@ def decrby( Decrement (or create an time-series and decrement) the latest sample's of a series. This command can be used as a counter or gauge that automatically gets history as a time series. - Args: - - key: - time-series key - value: - Numeric data value of the sample - timestamp: - Timestamp of the sample. * can be used for automatic timestamp (using the system clock). - retention_msecs: - Maximum age for samples compared to last event time (in milliseconds). - If None or 0 is passed then the series is not trimmed at all. - uncompressed: - Changes data storage from compressed (by default) to uncompressed - labels: - Set of label-value pairs that represent metadata labels of the key. - chunk_size: - Memory size, in bytes, allocated for each data chunk. - For more information: https://redis.io/commands/ts.decrby/ - """ # noqa + + Args: + key: + The time-series key. + value: + Numeric data value of the sample. + timestamp: + Timestamp of the sample. `*` can be used for automatic timestamp (using + the system clock). + retention_msecs: + Maximum age for samples compared to last event time (in milliseconds). + If `None` or `0` is passed then the series is not trimmed at all. + uncompressed: + Changes data storage from compressed (by default) to uncompressed. + labels: + Set of label-value pairs that represent metadata labels of the key. + chunk_size: + Memory size, in bytes, allocated for each data chunk. + """ params = [key, value] self._append_timestamp(params, timestamp) self._append_retention(params, retention_msecs) @@ -280,17 +341,16 @@ def delete(self, key: KeyT, from_time: int, to_time: int): """ Delete all samples between two timestamps for a given time series. - Args: - - key: - time-series key. - from_time: - Start timestamp for the range deletion. - to_time: - End timestamp for the range deletion. - For more information: https://redis.io/commands/ts.del/ - """ # noqa + + Args: + key: + The time-series key. + from_time: + Start timestamp for the range deletion. + to_time: + End timestamp for the range deletion. + """ return self.execute_command(DEL_CMD, key, from_time, to_time) def createrule( @@ -304,24 +364,23 @@ def createrule( """ Create a compaction rule from values added to `source_key` into `dest_key`. - Args: - - source_key: - Key name for source time series - dest_key: - Key name for destination (compacted) time series - aggregation_type: - Aggregation type: One of the following: - [`avg`, `sum`, `min`, `max`, `range`, `count`, `first`, `last`, `std.p`, - `std.s`, `var.p`, `var.s`, `twa`] - bucket_size_msec: - Duration of each bucket, in milliseconds - align_timestamp: - Assure that there is a bucket that starts at exactly align_timestamp and - align all other buckets accordingly. - For more information: https://redis.io/commands/ts.createrule/ - """ # noqa + + Args: + source_key: + Key name for source time series. + dest_key: + Key name for destination (compacted) time series. + aggregation_type: + Aggregation type: One of the following: + [`avg`, `sum`, `min`, `max`, `range`, `count`, `first`, `last`, `std.p`, + `std.s`, `var.p`, `var.s`, `twa`] + bucket_size_msec: + Duration of each bucket, in milliseconds. + align_timestamp: + Assure that there is a bucket that starts at exactly align_timestamp and + align all other buckets accordingly. + """ params = [source_key, dest_key] self._append_aggregation(params, aggregation_type, bucket_size_msec) if align_timestamp is not None: @@ -331,10 +390,10 @@ def createrule( def deleterule(self, source_key: KeyT, dest_key: KeyT): """ - Delete a compaction rule from `source_key` to `dest_key`.. + Delete a compaction rule from `source_key` to `dest_key`. For more information: https://redis.io/commands/ts.deleterule/ - """ # noqa + """ return self.execute_command(DELETERULE_CMD, source_key, dest_key) def __range_params( @@ -383,42 +442,46 @@ def range( empty: Optional[bool] = False, ): """ - Query a range in forward direction for a specific time-serie. - - Args: - - key: - Key name for timeseries. - from_time: - Start timestamp for the range query. - can be used to express the minimum possible timestamp (0). - to_time: - End timestamp for range query, + can be used to express the maximum possible timestamp. - count: - Limits the number of returned samples. - aggregation_type: - Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, - `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, `twa`] - bucket_size_msec: - Time bucket for aggregation in milliseconds. - filter_by_ts: - List of timestamps to filter the result by specific timestamps. - filter_by_min_value: - Filter result by minimum value (must mention also filter by_max_value). - filter_by_max_value: - Filter result by maximum value (must mention also filter by_min_value). - align: - Timestamp for alignment control for aggregation. - latest: - Used when a time series is a compaction, reports the compacted value of the - latest possibly partial bucket - bucket_timestamp: - Controls how bucket timestamps are reported. Can be one of [`-`, `low`, `+`, - `high`, `~`, `mid`]. - empty: - Reports aggregations for empty buckets. + Query a range in forward direction for a specific time-series. For more information: https://redis.io/commands/ts.range/ - """ # noqa + + Args: + key: + Key name for timeseries. + from_time: + Start timestamp for the range query. `-` can be used to express the + minimum possible timestamp (0). + to_time: + End timestamp for range query, `+` can be used to express the maximum + possible timestamp. + count: + Limits the number of returned samples. + aggregation_type: + Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, + `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, + `twa`] + bucket_size_msec: + Time bucket for aggregation in milliseconds. + filter_by_ts: + List of timestamps to filter the result by specific timestamps. + filter_by_min_value: + Filter result by minimum value (must mention also + `filter by_max_value`). + filter_by_max_value: + Filter result by maximum value (must mention also + `filter by_min_value`). + align: + Timestamp for alignment control for aggregation. + latest: + Used when a time series is a compaction, reports the compacted value of + the latest possibly partial bucket. + bucket_timestamp: + Controls how bucket timestamps are reported. Can be one of [`-`, `low`, + `+`, `high`, `~`, `mid`]. + empty: + Reports aggregations for empty buckets. + """ params = self.__range_params( key, from_time, @@ -457,40 +520,44 @@ def revrange( **Note**: This command is only available since RedisTimeSeries >= v1.4 - Args: - - key: - Key name for timeseries. - from_time: - Start timestamp for the range query. - can be used to express the minimum possible timestamp (0). - to_time: - End timestamp for range query, + can be used to express the maximum possible timestamp. - count: - Limits the number of returned samples. - aggregation_type: - Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, - `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, `twa`] - bucket_size_msec: - Time bucket for aggregation in milliseconds. - filter_by_ts: - List of timestamps to filter the result by specific timestamps. - filter_by_min_value: - Filter result by minimum value (must mention also filter_by_max_value). - filter_by_max_value: - Filter result by maximum value (must mention also filter_by_min_value). - align: - Timestamp for alignment control for aggregation. - latest: - Used when a time series is a compaction, reports the compacted value of the - latest possibly partial bucket - bucket_timestamp: - Controls how bucket timestamps are reported. Can be one of [`-`, `low`, `+`, - `high`, `~`, `mid`]. - empty: - Reports aggregations for empty buckets. - For more information: https://redis.io/commands/ts.revrange/ - """ # noqa + + Args: + key: + Key name for timeseries. + from_time: + Start timestamp for the range query. `-` can be used to express the + minimum possible timestamp (0). + to_time: + End timestamp for range query, `+` can be used to express the maximum + possible timestamp. + count: + Limits the number of returned samples. + aggregation_type: + Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, + `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, + `twa`] + bucket_size_msec: + Time bucket for aggregation in milliseconds. + filter_by_ts: + List of timestamps to filter the result by specific timestamps. + filter_by_min_value: + Filter result by minimum value (must mention also + `filter_by_max_value`). + filter_by_max_value: + Filter result by maximum value (must mention also + `filter_by_min_value`). + align: + Timestamp for alignment control for aggregation. + latest: + Used when a time series is a compaction, reports the compacted value of + the latest possibly partial bucket. + bucket_timestamp: + Controls how bucket timestamps are reported. Can be one of [`-`, `low`, + `+`, `high`, `~`, `mid`]. + empty: + Reports aggregations for empty buckets. + """ params = self.__range_params( key, from_time, @@ -567,49 +634,55 @@ def mrange( """ Query a range across multiple time-series by filters in forward direction. - Args: - - from_time: - Start timestamp for the range query. `-` can be used to express the minimum possible timestamp (0). - to_time: - End timestamp for range query, `+` can be used to express the maximum possible timestamp. - filters: - filter to match the time-series labels. - count: - Limits the number of returned samples. - aggregation_type: - Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, - `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, `twa`] - bucket_size_msec: - Time bucket for aggregation in milliseconds. - with_labels: - Include in the reply all label-value pairs representing metadata labels of the time series. - filter_by_ts: - List of timestamps to filter the result by specific timestamps. - filter_by_min_value: - Filter result by minimum value (must mention also filter_by_max_value). - filter_by_max_value: - Filter result by maximum value (must mention also filter_by_min_value). - groupby: - Grouping by fields the results (must mention also reduce). - reduce: - Applying reducer functions on each group. Can be one of [`avg` `sum`, `min`, - `max`, `range`, `count`, `std.p`, `std.s`, `var.p`, `var.s`]. - select_labels: - Include in the reply only a subset of the key-value pair labels of a series. - align: - Timestamp for alignment control for aggregation. - latest: - Used when a time series is a compaction, reports the compacted - value of the latest possibly partial bucket - bucket_timestamp: - Controls how bucket timestamps are reported. Can be one of [`-`, `low`, `+`, - `high`, `~`, `mid`]. - empty: - Reports aggregations for empty buckets. - For more information: https://redis.io/commands/ts.mrange/ - """ # noqa + + Args: + from_time: + Start timestamp for the range query. `-` can be used to express the + minimum possible timestamp (0). + to_time: + End timestamp for range query, `+` can be used to express the maximum + possible timestamp. + filters: + Filter to match the time-series labels. + count: + Limits the number of returned samples. + aggregation_type: + Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, + `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, + `twa`] + bucket_size_msec: + Time bucket for aggregation in milliseconds. + with_labels: + Include in the reply all label-value pairs representing metadata labels + of the time series. + filter_by_ts: + List of timestamps to filter the result by specific timestamps. + filter_by_min_value: + Filter result by minimum value (must mention also + `filter_by_max_value`). + filter_by_max_value: + Filter result by maximum value (must mention also + `filter_by_min_value`). + groupby: + Grouping by fields the results (must mention also `reduce`). + reduce: + Applying reducer functions on each group. Can be one of [`avg` `sum`, + `min`, `max`, `range`, `count`, `std.p`, `std.s`, `var.p`, `var.s`]. + select_labels: + Include in the reply only a subset of the key-value pair labels of a + series. + align: + Timestamp for alignment control for aggregation. + latest: + Used when a time series is a compaction, reports the compacted value of + the latest possibly partial bucket. + bucket_timestamp: + Controls how bucket timestamps are reported. Can be one of [`-`, `low`, + `+`, `high`, `~`, `mid`]. + empty: + Reports aggregations for empty buckets. + """ params = self.__mrange_params( aggregation_type, bucket_size_msec, @@ -655,49 +728,55 @@ def mrevrange( """ Query a range across multiple time-series by filters in reverse direction. - Args: - - from_time: - Start timestamp for the range query. - can be used to express the minimum possible timestamp (0). - to_time: - End timestamp for range query, + can be used to express the maximum possible timestamp. - filters: - Filter to match the time-series labels. - count: - Limits the number of returned samples. - aggregation_type: - Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, - `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, `twa`] - bucket_size_msec: - Time bucket for aggregation in milliseconds. - with_labels: - Include in the reply all label-value pairs representing metadata labels of the time series. - filter_by_ts: - List of timestamps to filter the result by specific timestamps. - filter_by_min_value: - Filter result by minimum value (must mention also filter_by_max_value). - filter_by_max_value: - Filter result by maximum value (must mention also filter_by_min_value). - groupby: - Grouping by fields the results (must mention also reduce). - reduce: - Applying reducer functions on each group. Can be one of [`avg` `sum`, `min`, - `max`, `range`, `count`, `std.p`, `std.s`, `var.p`, `var.s`]. - select_labels: - Include in the reply only a subset of the key-value pair labels of a series. - align: - Timestamp for alignment control for aggregation. - latest: - Used when a time series is a compaction, reports the compacted - value of the latest possibly partial bucket - bucket_timestamp: - Controls how bucket timestamps are reported. Can be one of [`-`, `low`, `+`, - `high`, `~`, `mid`]. - empty: - Reports aggregations for empty buckets. - For more information: https://redis.io/commands/ts.mrevrange/ - """ # noqa + + Args: + from_time: + Start timestamp for the range query. '-' can be used to express the + minimum possible timestamp (0). + to_time: + End timestamp for range query, '+' can be used to express the maximum + possible timestamp. + filters: + Filter to match the time-series labels. + count: + Limits the number of returned samples. + aggregation_type: + Optional aggregation type. Can be one of [`avg`, `sum`, `min`, `max`, + `range`, `count`, `first`, `last`, `std.p`, `std.s`, `var.p`, `var.s`, + `twa`]. + bucket_size_msec: + Time bucket for aggregation in milliseconds. + with_labels: + Include in the reply all label-value pairs representing metadata labels + of the time series. + filter_by_ts: + List of timestamps to filter the result by specific timestamps. + filter_by_min_value: + Filter result by minimum value (must mention also + `filter_by_max_value`). + filter_by_max_value: + Filter result by maximum value (must mention also + `filter_by_min_value`). + groupby: + Grouping by fields the results (must mention also `reduce`). + reduce: + Applying reducer functions on each group. Can be one of [`avg` `sum`, + `min`, `max`, `range`, `count`, `std.p`, `std.s`, `var.p`, `var.s`]. + select_labels: + Include in the reply only a subset of the key-value pair labels of a + series. + align: + Timestamp for alignment control for aggregation. + latest: + Used when a time series is a compaction, reports the compacted value of + the latest possibly partial bucket. + bucket_timestamp: + Controls how bucket timestamps are reported. Can be one of [`-`, `low`, + `+`, `high`, `~`, `mid`]. + empty: + Reports aggregations for empty buckets. + """ params = self.__mrange_params( aggregation_type, bucket_size_msec, @@ -721,13 +800,16 @@ def mrevrange( return self.execute_command(MREVRANGE_CMD, *params) def get(self, key: KeyT, latest: Optional[bool] = False): - """# noqa + """ Get the last sample of `key`. - `latest` used when a time series is a compaction, reports the compacted - value of the latest (possibly partial) bucket For more information: https://redis.io/commands/ts.get/ - """ # noqa + + Args: + latest: + Used when a time series is a compaction, reports the compacted value of + the latest (possibly partial) bucket. + """ params = [key] self._append_latest(params, latest) return self.execute_command(GET_CMD, *params, keys=[key]) @@ -739,24 +821,24 @@ def mget( select_labels: Optional[List[str]] = None, latest: Optional[bool] = False, ): - """# noqa + """ Get the last samples matching the specific `filter`. - Args: - - filters: - Filter to match the time-series labels. - with_labels: - Include in the reply all label-value pairs representing metadata - labels of the time series. - select_labels: - Include in the reply only a subset of the key-value pair labels of a series. - latest: - Used when a time series is a compaction, reports the compacted - value of the latest possibly partial bucket - For more information: https://redis.io/commands/ts.mget/ - """ # noqa + + Args: + filters: + Filter to match the time-series labels. + with_labels: + Include in the reply all label-value pairs representing metadata labels + of the time series. + select_labels: + Include in the reply only a subset of the key-value pair labels o the + time series. + latest: + Used when a time series is a compaction, reports the compacted value of + the latest possibly partial bucket. + """ params = [] self._append_latest(params, latest) self._append_with_labels(params, with_labels, select_labels) @@ -765,19 +847,19 @@ def mget( return self.execute_command(MGET_CMD, *params) def info(self, key: KeyT): - """# noqa + """ Get information of `key`. For more information: https://redis.io/commands/ts.info/ - """ # noqa + """ return self.execute_command(INFO_CMD, key, keys=[key]) def queryindex(self, filters: List[str]): - """# noqa + """ Get all time series keys matching the `filter` list. For more information: https://redis.io/commands/ts.queryindex/ - """ # noq + """ return self.execute_command(QUERYINDEX_CMD, *filters) @staticmethod @@ -903,3 +985,20 @@ def _append_empty(params: List[str], empty: Optional[bool]): """Append EMPTY property to params.""" if empty: params.append("EMPTY") + + @staticmethod + def _append_ignore_filters( + params: List[str], + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, + ): + """Append insertion filters to params.""" + if (ignore_max_time_diff is None) != (ignore_max_val_diff is None): + raise ValueError( + "Both ignore_max_time_diff and ignore_max_val_diff must be set." + ) + + if ignore_max_time_diff is not None and ignore_max_val_diff is not None: + params.extend( + ["IGNORE", str(ignore_max_time_diff), str(ignore_max_val_diff)] + ) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 6b59967f3c..74100a625f 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -3,8 +3,8 @@ from time import sleep import pytest -import redis +import redis from .conftest import assert_resp_response, is_resp2_connection, skip_ifmodversion_lt @@ -970,3 +970,83 @@ def test_uncompressed(client): assert compressed_info.memory_usage != uncompressed_info.memory_usage else: assert compressed_info["memoryUsage"] != uncompressed_info["memoryUsage"] + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_create_with_insertion_filters(client): + client.ts().create( + "time-series-1", + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + assert 1000 == client.ts().add("time-series-1", 1000, 1.0) + assert 1010 == client.ts().add("time-series-1", 1010, 11.0) + # Within 5 ms of the last timestamp and value diff less than 10.0 + assert 1010 == client.ts().add("time-series-1", 1013, 10.0) + # Value difference less than 10.0, but timestamp diff larger than 5 ms + assert 1020 == client.ts().add("time-series-1", 1020, 11.5) + # Timestamp diff less than 5 ms, but value diff larger than 10.0 + assert 1021 == client.ts().add("time-series-1", 1021, 22.0) + + data_points = client.ts().range("time-series-1", '-', '+') + expected_points = [(1000, 1.0), (1010, 11.0), (1020, 11.5), (1021, 22.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_create_with_insertion_filters_other_duplicate_policy(client): + client.ts().create( + "time-series-1", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + assert 1000 == client.ts().add("time-series-1", 1000, 1.0) + assert 1010 == client.ts().add("time-series-1", 1010, 11.0) + # Within 5 ms of the last timestamp and value diff less than 10.0. + # Still accepted because the duplicate_policy is not `last`. + assert 1013 == client.ts().add("time-series-1", 1013, 10.0) + + data_points = client.ts().range("time-series-1", '-', '+') + expected_points = [(1000, 1.0), (1010, 11.0), (1013, 10)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_alter_with_insertion_filters(client): + assert 1000 == client.ts().add("time-series-1", 1000, 1.0) + assert 1010 == client.ts().add("time-series-1", 1010, 11.0) + assert 1013 == client.ts().add("time-series-1", 1013, 10.0) + + client.ts().alter( + "time-series-1", + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0 + ) + + # Within 5 ms of the last timestamp and value diff less than 10.0. + assert 1013 == client.ts().add("time-series-1", 1015, 11.5) + + data_points = client.ts().range("time-series-1", '-', '+') + expected_points = [(1000, 1.0), (1010, 11.0), (1013, 10.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_add_with_insertion_filters(client): + assert 1000 == client.ts().add( + "time-series-1", + 1000, + 1.0, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + # Within 5 ms of the last timestamp and value diff less than 10.0. + assert 1000 == client.ts().add("time-series-1", 1004, 3.0) + + data_points = client.ts().range("time-series-1", '-', '+') + expected_points = [(1000, 1.0)] + assert expected_points == data_points From c5a535fd743a38b8d41290a5e6e2685ea76a0728 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Tue, 21 May 2024 14:29:22 +0300 Subject: [PATCH 02/10] Cover TS.INCRBY and TS.DECRBY --- redis/commands/timeseries/commands.py | 122 +++++++++++++++++++++----- tests/test_timeseries.py | 82 ++++++++++++----- 2 files changed, 158 insertions(+), 46 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index c24f6e87a6..1622598b8e 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -88,7 +88,7 @@ def create( self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) - self._append_duplicate_policy(params, CREATE_CMD, duplicate_policy) + self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) @@ -98,6 +98,7 @@ def alter( self, key: KeyT, retention_msecs: Optional[int] = None, + uncompressed: Optional[bool] = False, labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, duplicate_policy: Optional[str] = None, @@ -117,6 +118,8 @@ def alter( retention_msecs: Maximum age for samples, compared to the highest reported timestamp in milliseconds. If None or 0 is passed, the series is not trimmed at all. + uncompressed: + Changes data storage from compressed (default) to uncompressed. labels: A dictionary of label-value pairs that represent metadata labels of the key. @@ -153,8 +156,9 @@ def alter( """ params = [key] self._append_retention(params, retention_msecs) + self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) - self._append_duplicate_policy(params, ALTER_CMD, duplicate_policy) + self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) @@ -172,6 +176,7 @@ def add( duplicate_policy: Optional[str] = None, ignore_max_time_diff: Optional[int] = None, ignore_max_val_diff: Optional[Number] = None, + on_duplicate: Optional[str] = None, ): """ Append (or create and append) a new sample to a time series. @@ -225,14 +230,18 @@ def add( is lower than this threshold, the new entry is ignored. Only applicable if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is also set. Available since RedisTimeSeries version 1.12.0. + on_duplicate: + Use a specific duplicate policy for the specified timestamp. Overrides + the duplicate policy set by `duplicate_policy`. """ params = [key, timestamp, value] self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) - self._append_duplicate_policy(params, ADD_CMD, duplicate_policy) + self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_on_duplicate(params, on_duplicate) return self.execute_command(ADD_CMD, *params) @@ -260,6 +269,9 @@ def incrby( uncompressed: Optional[bool] = False, labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, + duplicate_policy: Optional[str] = None, + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, ): """ Increment (or create an time-series and increment) the latest sample's of a series. @@ -276,21 +288,52 @@ def incrby( Timestamp of the sample. `*` can be used for automatic timestamp (using the system clock). retention_msecs: - Maximum age for samples compared to last event time (in milliseconds). - If `None` or `0` is passed then the series is not trimmed at all. + Maximum age for samples, compared to the highest reported timestamp in + milliseconds. If None or 0 is passed, the series is not trimmed at all. uncompressed: - Changes data storage from compressed (by default) to uncompressed. + Changes data storage from compressed (default) to uncompressed. labels: - Set of label-value pairs that represent metadata labels of the key. + A dictionary of label-value pairs that represent metadata labels of the + key. chunk_size: - Memory size, in bytes, allocated for each data chunk. + Memory size, in bytes, allocated for each data chunk. Must be a multiple + of 8 in the range [128..1048576]. + duplicate_policy: + Policy for handling multiple samples with identical timestamps. Can be + one of: + - 'block': An error will occur for any out of order sample. + - 'first': Ignore the new value. + - 'last': Override with the latest value. + - 'min': Only override if the value is lower than the existing + value. + - 'max': Only override if the value is higher than the existing + value. + - 'sum': If a previous sample exists, add the new sample to it so + that the updated value is equal to (previous + new). If no + previous sample exists, set the updated value equal to the new + value. + ignore_max_time_diff: + A non-negative integer value, in milliseconds, that sets an ignore + threshold for added timestamps. If the difference between the last + timestamp and the new timestamp is lower than this threshold, the new + entry is ignored. Only applicable if `duplicate_policy` is set to + `last`, and if `ignore_max_val_diff` is also set. Available since + RedisTimeSeries version 1.12.0. + ignore_max_val_diff: + A non-negative floating point value, that sets an ignore threshold for + added values. If the difference between the last value and the new value + is lower than this threshold, the new entry is ignored. Only applicable + if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is + also set. Available since RedisTimeSeries version 1.12.0. """ params = [key, value] self._append_timestamp(params, timestamp) self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) + self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) + self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) return self.execute_command(INCRBY_CMD, *params) @@ -303,6 +346,9 @@ def decrby( uncompressed: Optional[bool] = False, labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, + duplicate_policy: Optional[str] = None, + ignore_max_time_diff: Optional[int] = None, + ignore_max_val_diff: Optional[Number] = None, ): """ Decrement (or create an time-series and decrement) the latest sample's of a series. @@ -319,21 +365,52 @@ def decrby( Timestamp of the sample. `*` can be used for automatic timestamp (using the system clock). retention_msecs: - Maximum age for samples compared to last event time (in milliseconds). - If `None` or `0` is passed then the series is not trimmed at all. + Maximum age for samples, compared to the highest reported timestamp in + milliseconds. If None or 0 is passed, the series is not trimmed at all. uncompressed: - Changes data storage from compressed (by default) to uncompressed. + Changes data storage from compressed (default) to uncompressed. labels: - Set of label-value pairs that represent metadata labels of the key. + A dictionary of label-value pairs that represent metadata labels of the + key. chunk_size: - Memory size, in bytes, allocated for each data chunk. + Memory size, in bytes, allocated for each data chunk. Must be a multiple + of 8 in the range [128..1048576]. + duplicate_policy: + Policy for handling multiple samples with identical timestamps. Can be + one of: + - 'block': An error will occur for any out of order sample. + - 'first': Ignore the new value. + - 'last': Override with the latest value. + - 'min': Only override if the value is lower than the existing + value. + - 'max': Only override if the value is higher than the existing + value. + - 'sum': If a previous sample exists, add the new sample to it so + that the updated value is equal to (previous + new). If no + previous sample exists, set the updated value equal to the new + value. + ignore_max_time_diff: + A non-negative integer value, in milliseconds, that sets an ignore + threshold for added timestamps. If the difference between the last + timestamp and the new timestamp is lower than this threshold, the new + entry is ignored. Only applicable if `duplicate_policy` is set to + `last`, and if `ignore_max_val_diff` is also set. Available since + RedisTimeSeries version 1.12.0. + ignore_max_val_diff: + A non-negative floating point value, that sets an ignore threshold for + added values. If the difference between the last value and the new value + is lower than this threshold, the new entry is ignored. Only applicable + if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is + also set. Available since RedisTimeSeries version 1.12.0. """ params = [key, value] self._append_timestamp(params, timestamp) self._append_retention(params, retention_msecs) self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) + self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) + self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) return self.execute_command(DECRBY_CMD, *params) @@ -942,17 +1019,16 @@ def _append_chunk_size(params: List[str], chunk_size: Optional[int]): params.extend(["CHUNK_SIZE", chunk_size]) @staticmethod - def _append_duplicate_policy( - params: List[str], command: Optional[str], duplicate_policy: Optional[str] - ): - """Append DUPLICATE_POLICY property to params on CREATE - and ON_DUPLICATE on ADD. - """ + def _append_duplicate_policy(params: List[str], duplicate_policy: Optional[str]): + """Append DUPLICATE_POLICY property to params.""" if duplicate_policy is not None: - if command == "TS.ADD": - params.extend(["ON_DUPLICATE", duplicate_policy]) - else: - params.extend(["DUPLICATE_POLICY", duplicate_policy]) + params.extend(["DUPLICATE_POLICY", duplicate_policy]) + + @staticmethod + def _append_on_duplicate(params: List[str], on_duplicate: Optional[str]): + """Append ON_DUPLICATE property to params.""" + if on_duplicate is not None: + params.extend(["ON_DUPLICATE", on_duplicate]) @staticmethod def _append_filer_by_ts(params: List[str], ts_list: Optional[List[int]]): diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 74100a625f..65f1a144ab 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -3,8 +3,8 @@ from time import sleep import pytest - import redis + from .conftest import assert_resp_response, is_resp2_connection, skip_ifmodversion_lt @@ -103,7 +103,7 @@ def test_add(client): @skip_ifmodversion_lt("1.4.0", "timeseries") -def test_add_duplicate_policy(client): +def test_add_on_duplicate(client): # Test for duplicate policy BLOCK assert 1 == client.ts().add("time-serie-add-ooo-block", 1, 5.0) with pytest.raises(Exception): @@ -111,30 +111,24 @@ def test_add_duplicate_policy(client): # Test for duplicate policy LAST assert 1 == client.ts().add("time-serie-add-ooo-last", 1, 5.0) - assert 1 == client.ts().add( - "time-serie-add-ooo-last", 1, 10.0, duplicate_policy="last" - ) + assert 1 == client.ts().add("time-serie-add-ooo-last", 1, 10.0, on_duplicate="last") assert 10.0 == client.ts().get("time-serie-add-ooo-last")[1] # Test for duplicate policy FIRST assert 1 == client.ts().add("time-serie-add-ooo-first", 1, 5.0) assert 1 == client.ts().add( - "time-serie-add-ooo-first", 1, 10.0, duplicate_policy="first" + "time-serie-add-ooo-first", 1, 10.0, on_duplicate="first" ) assert 5.0 == client.ts().get("time-serie-add-ooo-first")[1] # Test for duplicate policy MAX assert 1 == client.ts().add("time-serie-add-ooo-max", 1, 5.0) - assert 1 == client.ts().add( - "time-serie-add-ooo-max", 1, 10.0, duplicate_policy="max" - ) + assert 1 == client.ts().add("time-serie-add-ooo-max", 1, 10.0, on_duplicate="max") assert 10.0 == client.ts().get("time-serie-add-ooo-max")[1] # Test for duplicate policy MIN assert 1 == client.ts().add("time-serie-add-ooo-min", 1, 5.0) - assert 1 == client.ts().add( - "time-serie-add-ooo-min", 1, 10.0, duplicate_policy="min" - ) + assert 1 == client.ts().add("time-serie-add-ooo-min", 1, 10.0, on_duplicate="min") assert 5.0 == client.ts().get("time-serie-add-ooo-min")[1] @@ -982,14 +976,11 @@ def test_create_with_insertion_filters(client): ) assert 1000 == client.ts().add("time-series-1", 1000, 1.0) assert 1010 == client.ts().add("time-series-1", 1010, 11.0) - # Within 5 ms of the last timestamp and value diff less than 10.0 assert 1010 == client.ts().add("time-series-1", 1013, 10.0) - # Value difference less than 10.0, but timestamp diff larger than 5 ms assert 1020 == client.ts().add("time-series-1", 1020, 11.5) - # Timestamp diff less than 5 ms, but value diff larger than 10.0 assert 1021 == client.ts().add("time-series-1", 1021, 22.0) - data_points = client.ts().range("time-series-1", '-', '+') + data_points = client.ts().range("time-series-1", "-", "+") expected_points = [(1000, 1.0), (1010, 11.0), (1020, 11.5), (1021, 22.0)] assert expected_points == data_points @@ -1003,11 +994,10 @@ def test_create_with_insertion_filters_other_duplicate_policy(client): ) assert 1000 == client.ts().add("time-series-1", 1000, 1.0) assert 1010 == client.ts().add("time-series-1", 1010, 11.0) - # Within 5 ms of the last timestamp and value diff less than 10.0. # Still accepted because the duplicate_policy is not `last`. assert 1013 == client.ts().add("time-series-1", 1013, 10.0) - data_points = client.ts().range("time-series-1", '-', '+') + data_points = client.ts().range("time-series-1", "-", "+") expected_points = [(1000, 1.0), (1010, 11.0), (1013, 10)] assert expected_points == data_points @@ -1022,13 +1012,12 @@ def test_alter_with_insertion_filters(client): "time-series-1", duplicate_policy="last", ignore_max_time_diff=5, - ignore_max_val_diff=10.0 + ignore_max_val_diff=10.0, ) - # Within 5 ms of the last timestamp and value diff less than 10.0. assert 1013 == client.ts().add("time-series-1", 1015, 11.5) - data_points = client.ts().range("time-series-1", '-', '+') + data_points = client.ts().range("time-series-1", "-", "+") expected_points = [(1000, 1.0), (1010, 11.0), (1013, 10.0)] assert expected_points == data_points @@ -1044,9 +1033,56 @@ def test_add_with_insertion_filters(client): ignore_max_val_diff=10.0, ) - # Within 5 ms of the last timestamp and value diff less than 10.0. assert 1000 == client.ts().add("time-series-1", 1004, 3.0) - data_points = client.ts().range("time-series-1", '-', '+') + data_points = client.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 1.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_incrby_with_insertion_filters(client): + assert 1000 == client.ts().incrby( + "time-series-1", + 1.0, + timestamp=1000, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1000 == client.ts().incrby("time-series-1", 3.0, timestamp=1000) + + data_points = client.ts().range("time-series-1", "-", "+") expected_points = [(1000, 1.0)] assert expected_points == data_points + + assert 1000 == client.ts().incrby("time-series-1", 10.1, timestamp=1000) + + data_points = client.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 11.1)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_decrby_with_insertion_filters(client): + assert 1000 == client.ts().decrby( + "time-series-1", + 1.0, + timestamp=1000, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1000 == client.ts().decrby("time-series-1", 3.0, timestamp=1000) + + data_points = client.ts().range("time-series-1", "-", "+") + expected_points = [(1000, -1.0)] + assert expected_points == data_points + + assert 1000 == client.ts().decrby("time-series-1", 10.1, timestamp=1000) + + data_points = client.ts().range("time-series-1", "-", "+") + expected_points = [(1000, -11.1)] + assert expected_points == data_points From 32adbd0e6e25faf44a8618fe0258145b097e1c27 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Tue, 21 May 2024 14:39:47 +0300 Subject: [PATCH 03/10] Test for TS.MADD --- tests/test_timeseries.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 65f1a144ab..1bf6d73e5d 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -1086,3 +1086,26 @@ def test_decrby_with_insertion_filters(client): data_points = client.ts().range("time-series-1", "-", "+") expected_points = [(1000, -11.1)] assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +def test_madd_with_insertion_filters(client): + client.ts().create( + "time-series-1", + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + assert 1010 == client.ts().add("time-series-1", 1010, 1.0) + assert [1010, 1010, 1020, 1021] == client.ts().madd( + [ + ("time-series-1", 1011, 11.0), + ("time-series-1", 1013, 10.0), + ("time-series-1", 1020, 2.0), + ("time-series-1", 1021, 22.0), + ] + ) + + data_points = client.ts().range("time-series-1", "-", "+") + expected_points = [(1010, 1.0), (1020, 2.0), (1021, 22.0)] + assert expected_points == data_points From 16b1e0897136882169421cb846638532e86bfc25 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Tue, 21 May 2024 18:05:43 +0300 Subject: [PATCH 04/10] Streamline UNCOMPRESSED Use the documented way to disable compression, i.e. ENCODING UNCOMPRESSED. Remove the uncompressed flag from TS.ALTER. --- redis/commands/timeseries/commands.py | 6 +----- tests/test_timeseries.py | 7 +++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index 1622598b8e..05a1f2f506 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -98,7 +98,6 @@ def alter( self, key: KeyT, retention_msecs: Optional[int] = None, - uncompressed: Optional[bool] = False, labels: Optional[Dict[str, str]] = None, chunk_size: Optional[int] = None, duplicate_policy: Optional[str] = None, @@ -118,8 +117,6 @@ def alter( retention_msecs: Maximum age for samples, compared to the highest reported timestamp in milliseconds. If None or 0 is passed, the series is not trimmed at all. - uncompressed: - Changes data storage from compressed (default) to uncompressed. labels: A dictionary of label-value pairs that represent metadata labels of the key. @@ -156,7 +153,6 @@ def alter( """ params = [key] self._append_retention(params, retention_msecs) - self._append_uncompressed(params, uncompressed) self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) @@ -943,7 +939,7 @@ def queryindex(self, filters: List[str]): def _append_uncompressed(params: List[str], uncompressed: Optional[bool]): """Append UNCOMPRESSED tag to params.""" if uncompressed: - params.extend(["UNCOMPRESSED"]) + params.extend(["ENCODING", "UNCOMPRESSED"]) @staticmethod def _append_with_labels( diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 1bf6d73e5d..20f69ef977 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -958,12 +958,15 @@ def test_pipeline(client): def test_uncompressed(client): client.ts().create("compressed") client.ts().create("uncompressed", uncompressed=True) + for i in range(1000): + client.ts().add("compressed", i, i) + client.ts().add("uncompressed", i, i) compressed_info = client.ts().info("compressed") uncompressed_info = client.ts().info("uncompressed") if is_resp2_connection(client): - assert compressed_info.memory_usage != uncompressed_info.memory_usage + assert compressed_info.memory_usage < uncompressed_info.memory_usage else: - assert compressed_info["memoryUsage"] != uncompressed_info["memoryUsage"] + assert compressed_info["memoryUsage"] < uncompressed_info["memoryUsage"] @skip_ifmodversion_lt("1.12.0", "timeseries") From 88b055d5eef7e1bff4bc49aeeca244b1765a745c Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 22 May 2024 08:48:14 +0300 Subject: [PATCH 05/10] Fix linter errors --- redis/commands/timeseries/commands.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index 05a1f2f506..1ca64e3596 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -270,8 +270,9 @@ def incrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Increment (or create an time-series and increment) the latest sample's of a series. - This command can be used as a counter or gauge that automatically gets history as a time series. + Increment (or create a time-series and increment) the latest sample's of a + series. This command can be used as a counter or gauge that automatically gets + history as a time series. For more information: https://redis.io/commands/ts.incrby/ @@ -347,8 +348,9 @@ def decrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Decrement (or create an time-series and decrement) the latest sample's of a series. - This command can be used as a counter or gauge that automatically gets history as a time series. + Decrement (or create a time-series and decrement) the latest sample's of a + series. This command can be used as a counter or gauge that automatically gets + history as a time series. For more information: https://redis.io/commands/ts.decrby/ From a8200bce94298349e6256cc9293e1f8130d38afc Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 22 May 2024 09:43:21 +0300 Subject: [PATCH 06/10] Polish the documentation a bit more --- redis/commands/timeseries/commands.py | 152 +++++++++++++++++--------- tests/test_timeseries.py | 13 ++- 2 files changed, 111 insertions(+), 54 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index 1ca64e3596..a8d99adbf0 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -37,17 +37,17 @@ def create( ignore_max_val_diff: Optional[Number] = None, ): """ - Creates a new time-series. + Create a new time-series. - For more information, see the Redis command details: - https://redis.io/commands/ts.create/ + For more information see https://redis.io/commands/ts.create/ Args: key: The time-series key. retention_msecs: Maximum age for samples, compared to the highest reported timestamp in - milliseconds. If None or 0 is passed, the series is not trimmed at all. + milliseconds. If `None` or `0` is passed, the series is not trimmed at + all. uncompressed: Changes data storage from compressed (default) to uncompressed. labels: @@ -55,11 +55,12 @@ def create( key. chunk_size: Memory size, in bytes, allocated for each data chunk. Must be a multiple - of 8 in the range [128..1048576]. + of 8 in the range `[48..1048576]`. In earlier versions of the module the + minimum value was different. duplicate_policy: Policy for handling multiple samples with identical timestamps. Can be one of: - - 'block': An error will occur for any out of order sample. + - 'block': An error will occur and the new value will be ignored. - 'first': Ignore the new value. - 'last': Override with the latest value. - 'min': Only override if the value is lower than the existing @@ -105,28 +106,29 @@ def alter( ignore_max_val_diff: Optional[Number] = None, ): """ - Update the retention, chunk size, duplicate policy, and labels of an existing - time series. + Update an existing time series. - For more information, see the Redis command details: - https://redis.io/commands/ts.alter/ + For more information see https://redis.io/commands/ts.alter/ Args: key: The time-series key. retention_msecs: Maximum age for samples, compared to the highest reported timestamp in - milliseconds. If None or 0 is passed, the series is not trimmed at all. + milliseconds. If `None` or `0` is passed, the series is not trimmed at + all. labels: A dictionary of label-value pairs that represent metadata labels of the key. chunk_size: Memory size, in bytes, allocated for each data chunk. Must be a multiple - of 8 in the range [128..1048576]. + of 8 in the range `[48..1048576]`. In earlier versions of the module the + minimum value was different. Changing this value does not affect + existing chunks. duplicate_policy: Policy for handling multiple samples with identical timestamps. Can be one of: - - 'block': An error will occur for any out of order sample. + - 'block': An error will occur and the new value will be ignored. - 'first': Ignore the new value. - 'last': Override with the latest value. - 'min': Only override if the value is lower than the existing @@ -175,10 +177,10 @@ def add( on_duplicate: Optional[str] = None, ): """ - Append (or create and append) a new sample to a time series. + Append a sample to a time series. When the specified key does not exist, a new + time series is created. - For more information, see the Redis command details: - https://redis.io/commands/ts.add/ + For more information see https://redis.io/commands/ts.add/ Args: key: @@ -190,7 +192,8 @@ def add( Numeric data value of the sample. retention_msecs: Maximum age for samples, compared to the highest reported timestamp in - milliseconds. If None or 0 is passed, the series is not trimmed at all. + milliseconds. If `None` or `0` is passed, the series is not trimmed at + all. uncompressed: Changes data storage from compressed (default) to uncompressed. labels: @@ -198,11 +201,12 @@ def add( key. chunk_size: Memory size, in bytes, allocated for each data chunk. Must be a multiple - of 8 in the range [128..1048576]. + of 8 in the range `[48..1048576]`. In earlier versions of the module the + minimum value was different. duplicate_policy: Policy for handling multiple samples with identical timestamps. Can be one of: - - 'block': An error will occur for any out of order sample. + - 'block': An error will occur and the new value will be ignored. - 'first': Ignore the new value. - 'last': Override with the latest value. - 'min': Only override if the value is lower than the existing @@ -243,12 +247,26 @@ def add( def madd(self, ktv_tuples: List[Tuple[KeyT, Union[int, str], Number]]): """ - Append (or create and append) a new `value` to series `key` with `timestamp`. - Expects a list of `tuples` as (`key`,`timestamp`, `value`). + Append new samples to one or more time series. - Return value is an array with timestamps of insertions. + Each time series must already exist. - For more information: https://redis.io/commands/ts.madd/ + The method expects a list of tuples. Each tuple should contain three elements: + (`key`, `timestamp`, `value`). The `value` will be appended to the time series + identified by 'key', at the given 'timestamp'. + + For more information see https://redis.io/commands/ts.madd/ + + Args: + ktv_tuples: + A list of tuples, where each tuple contains: + - `key`: The key of the time series. + - `timestamp`: The timestamp at which the value should be appended. + - `value`: The value to append to the time series. + + Returns: + A list that contains, for each sample, either the timestamp that was used, + or an error, if the sample could not be added. """ params = [] for ktv in ktv_tuples: @@ -270,23 +288,31 @@ def incrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Increment (or create a time-series and increment) the latest sample's of a - series. This command can be used as a counter or gauge that automatically gets - history as a time series. + Increment the latest sample's of a series. When specified key does not exist, a + new time series is created. + + This command can be used as a counter or gauge that automatically gets history + as a time series. - For more information: https://redis.io/commands/ts.incrby/ + For more information see https://redis.io/commands/ts.incrby/ Args: key: The time-series key. value: - Numeric data value of the sample. + Numeric value to be added (addend). timestamp: Timestamp of the sample. `*` can be used for automatic timestamp (using - the system clock). + the system clock). `timestamp` must be equal to or higher than the + maximum existing timestamp in the series. When equal, the value of the + sample with the maximum existing timestamp is increased. If it is + higher, a new sample with a timestamp set to `timestamp` is created, and + its value is set to the value of the sample with the maximum existing + timestamp plus the addend. retention_msecs: Maximum age for samples, compared to the highest reported timestamp in - milliseconds. If None or 0 is passed, the series is not trimmed at all. + milliseconds. If `None` or `0` is passed, the series is not trimmed at + all. uncompressed: Changes data storage from compressed (default) to uncompressed. labels: @@ -294,11 +320,12 @@ def incrby( key. chunk_size: Memory size, in bytes, allocated for each data chunk. Must be a multiple - of 8 in the range [128..1048576]. + of 8 in the range `[48..1048576]`. In earlier versions of the module the + minimum value was different. duplicate_policy: Policy for handling multiple samples with identical timestamps. Can be one of: - - 'block': An error will occur for any out of order sample. + - 'block': An error will occur and the new value will be ignored. - 'first': Ignore the new value. - 'last': Override with the latest value. - 'min': Only override if the value is lower than the existing @@ -322,6 +349,9 @@ def incrby( is lower than this threshold, the new entry is ignored. Only applicable if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is also set. Available since RedisTimeSeries version 1.12.0. + + Returns: + The timestamp of the sample that was modified or added. """ params = [key, value] self._append_timestamp(params, timestamp) @@ -348,23 +378,31 @@ def decrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Decrement (or create a time-series and decrement) the latest sample's of a - series. This command can be used as a counter or gauge that automatically gets - history as a time series. + Decrement the latest sample's of a series. When specified key does not exist, a + new time series is created. - For more information: https://redis.io/commands/ts.decrby/ + This command can be used as a counter or gauge that automatically gets history + as a time series. + + For more information see https://redis.io/commands/ts.decrby/ Args: key: The time-series key. value: - Numeric data value of the sample. + Numeric value to subtract (subtrahend). timestamp: Timestamp of the sample. `*` can be used for automatic timestamp (using - the system clock). + the system clock). `timestamp` must be equal to or higher than the + maximum existing timestamp in the series. When equal, the value of the + sample with the maximum existing timestamp is decreased. If it is + higher, a new sample with a timestamp set to `timestamp` is created, and + its value is set to the value of the sample with the maximum existing + timestamp minus subtrahend. retention_msecs: Maximum age for samples, compared to the highest reported timestamp in - milliseconds. If None or 0 is passed, the series is not trimmed at all. + milliseconds. If `None` or `0` is passed, the series is not trimmed at + all. uncompressed: Changes data storage from compressed (default) to uncompressed. labels: @@ -372,11 +410,12 @@ def decrby( key. chunk_size: Memory size, in bytes, allocated for each data chunk. Must be a multiple - of 8 in the range [128..1048576]. + of 8 in the range `[48..1048576]`. In earlier versions of the module the + minimum value was different. duplicate_policy: Policy for handling multiple samples with identical timestamps. Can be one of: - - 'block': An error will occur for any out of order sample. + - 'block': An error will occur and the new value will be ignored. - 'first': Ignore the new value. - 'last': Override with the latest value. - 'min': Only override if the value is lower than the existing @@ -400,6 +439,9 @@ def decrby( is lower than this threshold, the new entry is ignored. Only applicable if `duplicate_policy` is set to `last`, and if `ignore_max_time_diff` is also set. Available since RedisTimeSeries version 1.12.0. + + Returns: + The timestamp of the sample that was modified or added. """ params = [key, value] self._append_timestamp(params, timestamp) @@ -416,7 +458,10 @@ def delete(self, key: KeyT, from_time: int, to_time: int): """ Delete all samples between two timestamps for a given time series. - For more information: https://redis.io/commands/ts.del/ + The given timestamp interval is closed (inclusive), meaning that samples whose + timestamp equals `from_time` or `to_time` are also deleted. + + For more information see https://redis.io/commands/ts.del/ Args: key: @@ -425,6 +470,9 @@ def delete(self, key: KeyT, from_time: int, to_time: int): Start timestamp for the range deletion. to_time: End timestamp for the range deletion. + + Returns: + The number of samples deleted. """ return self.execute_command(DEL_CMD, key, from_time, to_time) @@ -439,7 +487,7 @@ def createrule( """ Create a compaction rule from values added to `source_key` into `dest_key`. - For more information: https://redis.io/commands/ts.createrule/ + For more information see https://redis.io/commands/ts.createrule/ Args: source_key: @@ -467,7 +515,7 @@ def deleterule(self, source_key: KeyT, dest_key: KeyT): """ Delete a compaction rule from `source_key` to `dest_key`. - For more information: https://redis.io/commands/ts.deleterule/ + For more information see https://redis.io/commands/ts.deleterule/ """ return self.execute_command(DELETERULE_CMD, source_key, dest_key) @@ -519,7 +567,7 @@ def range( """ Query a range in forward direction for a specific time-series. - For more information: https://redis.io/commands/ts.range/ + For more information see https://redis.io/commands/ts.range/ Args: key: @@ -595,7 +643,7 @@ def revrange( **Note**: This command is only available since RedisTimeSeries >= v1.4 - For more information: https://redis.io/commands/ts.revrange/ + For more information see https://redis.io/commands/ts.revrange/ Args: key: @@ -709,7 +757,7 @@ def mrange( """ Query a range across multiple time-series by filters in forward direction. - For more information: https://redis.io/commands/ts.mrange/ + For more information see https://redis.io/commands/ts.mrange/ Args: from_time: @@ -803,7 +851,7 @@ def mrevrange( """ Query a range across multiple time-series by filters in reverse direction. - For more information: https://redis.io/commands/ts.mrevrange/ + For more information see https://redis.io/commands/ts.mrevrange/ Args: from_time: @@ -878,7 +926,7 @@ def get(self, key: KeyT, latest: Optional[bool] = False): """ Get the last sample of `key`. - For more information: https://redis.io/commands/ts.get/ + For more information see https://redis.io/commands/ts.get/ Args: latest: @@ -899,7 +947,7 @@ def mget( """ Get the last samples matching the specific `filter`. - For more information: https://redis.io/commands/ts.mget/ + For more information see https://redis.io/commands/ts.mget/ Args: filters: @@ -925,7 +973,7 @@ def info(self, key: KeyT): """ Get information of `key`. - For more information: https://redis.io/commands/ts.info/ + For more information see https://redis.io/commands/ts.info/ """ return self.execute_command(INFO_CMD, key, keys=[key]) @@ -933,7 +981,7 @@ def queryindex(self, filters: List[str]): """ Get all time series keys matching the `filter` list. - For more information: https://redis.io/commands/ts.queryindex/ + For more information see https://redis.io/commands/ts.queryindex/ """ return self.execute_command(QUERYINDEX_CMD, *filters) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 20f69ef977..df6331f599 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -4,6 +4,7 @@ import pytest import redis +from redis import ResponseError from .conftest import assert_resp_response, is_resp2_connection, skip_ifmodversion_lt @@ -137,6 +138,14 @@ def test_madd(client): assert [1, 2, 3] == client.ts().madd([("a", 1, 5), ("a", 2, 10), ("a", 3, 15)]) +def test_madd_missing_timeseries(client): + response = client.ts().madd([("a", 1, 5), ("a", 2, 10)]) + assert isinstance(response, list) + assert len(response) == 2 + assert isinstance(response[0], ResponseError) + assert isinstance(response[1], ResponseError) + + def test_incrby_decrby(client): for _ in range(100): assert client.ts().incrby(1, 1) @@ -192,8 +201,8 @@ def test_create_and_delete_rule(client): def test_del_range(client): try: client.ts().delete("test", 0, 100) - except Exception as e: - assert e.__str__() != "" + except ResponseError as e: + assert "key does not exist" in str(e) for i in range(100): client.ts().add(1, i, i % 7) From 20f71723a962ddaab7aa882b130328182d1ea757 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 22 May 2024 09:49:06 +0300 Subject: [PATCH 07/10] More cleanup --- redis/commands/timeseries/commands.py | 30 ++++++++++++++++++--------- tests/test_timeseries.py | 7 +++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/redis/commands/timeseries/commands.py b/redis/commands/timeseries/commands.py index a8d99adbf0..f8dfe8b5c0 100644 --- a/redis/commands/timeseries/commands.py +++ b/redis/commands/timeseries/commands.py @@ -91,7 +91,9 @@ def create( self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) - self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_insertion_filters( + params, ignore_max_time_diff, ignore_max_val_diff + ) return self.execute_command(CREATE_CMD, *params) @@ -158,7 +160,9 @@ def alter( self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) - self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_insertion_filters( + params, ignore_max_time_diff, ignore_max_val_diff + ) return self.execute_command(ALTER_CMD, *params) @@ -240,7 +244,9 @@ def add( self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) - self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_insertion_filters( + params, ignore_max_time_diff, ignore_max_val_diff + ) self._append_on_duplicate(params, on_duplicate) return self.execute_command(ADD_CMD, *params) @@ -288,8 +294,8 @@ def incrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Increment the latest sample's of a series. When specified key does not exist, a - new time series is created. + Increment the latest sample's of a series. When the specified key does not + exist, a new time series is created. This command can be used as a counter or gauge that automatically gets history as a time series. @@ -360,7 +366,9 @@ def incrby( self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) - self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_insertion_filters( + params, ignore_max_time_diff, ignore_max_val_diff + ) return self.execute_command(INCRBY_CMD, *params) @@ -378,8 +386,8 @@ def decrby( ignore_max_val_diff: Optional[Number] = None, ): """ - Decrement the latest sample's of a series. When specified key does not exist, a - new time series is created. + Decrement the latest sample's of a series. When the specified key does not + exist, a new time series is created. This command can be used as a counter or gauge that automatically gets history as a time series. @@ -450,7 +458,9 @@ def decrby( self._append_chunk_size(params, chunk_size) self._append_duplicate_policy(params, duplicate_policy) self._append_labels(params, labels) - self._append_ignore_filters(params, ignore_max_time_diff, ignore_max_val_diff) + self._append_insertion_filters( + params, ignore_max_time_diff, ignore_max_val_diff + ) return self.execute_command(DECRBY_CMD, *params) @@ -1109,7 +1119,7 @@ def _append_empty(params: List[str], empty: Optional[bool]): params.append("EMPTY") @staticmethod - def _append_ignore_filters( + def _append_insertion_filters( params: List[str], ignore_max_time_diff: Optional[int] = None, ignore_max_val_diff: Optional[Number] = None, diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index df6331f599..7a9e2f1f60 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -4,7 +4,6 @@ import pytest import redis -from redis import ResponseError from .conftest import assert_resp_response, is_resp2_connection, skip_ifmodversion_lt @@ -142,8 +141,8 @@ def test_madd_missing_timeseries(client): response = client.ts().madd([("a", 1, 5), ("a", 2, 10)]) assert isinstance(response, list) assert len(response) == 2 - assert isinstance(response[0], ResponseError) - assert isinstance(response[1], ResponseError) + assert isinstance(response[0], redis.ResponseError) + assert isinstance(response[1], redis.ResponseError) def test_incrby_decrby(client): @@ -201,7 +200,7 @@ def test_create_and_delete_rule(client): def test_del_range(client): try: client.ts().delete("test", 0, 100) - except ResponseError as e: + except redis.ResponseError as e: assert "key does not exist" in str(e) for i in range(100): From 7c68da1c201fac50c9e05e0c8f58d830c43a23f3 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 12 Jun 2024 11:04:02 +0300 Subject: [PATCH 08/10] Add async tests --- .github/workflows/integration.yaml | 2 + docker-compose.yml | 2 +- tests/test_asyncio/test_timeseries.py | 134 +++++++++++++++++++++++--- tests/test_timeseries.py | 14 +-- 4 files changed, 129 insertions(+), 23 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 8f60efe6c7..a7a5513c64 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -62,6 +62,7 @@ jobs: connection-type: ['hiredis', 'plain'] env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true + REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc1 name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests steps: - uses: actions/checkout@v4 @@ -116,6 +117,7 @@ jobs: protocol: ['3'] env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true + REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc1 name: RESP3 [${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}}] steps: - uses: actions/checkout@v4 diff --git a/docker-compose.yml b/docker-compose.yml index 09418ed094..72c43c2252 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,7 +105,7 @@ services: - all redis-stack: - image: redis/redis-stack-server:edge + image: ${REDIS_STACK_IMAGE:-redis/redis-stack-server:edge} container_name: redis-stack ports: - 6479:6379 diff --git a/tests/test_asyncio/test_timeseries.py b/tests/test_asyncio/test_timeseries.py index 0c78ce0941..93298bfa90 100644 --- a/tests/test_asyncio/test_timeseries.py +++ b/tests/test_asyncio/test_timeseries.py @@ -75,7 +75,7 @@ async def test_alter(decoded_r: redis.Redis): @pytest.mark.redismod @skip_ifmodversion_lt("1.4.0", "timeseries") -async def test_alter_diplicate_policy(decoded_r: redis.Redis): +async def test_alter_duplicate_policy(decoded_r: redis.Redis): assert await decoded_r.ts().create(1) info = await decoded_r.ts().info(1) assert_resp_response( @@ -117,12 +117,12 @@ async def test_add_duplicate_policy(r: redis.Redis): # Test for duplicate policy BLOCK assert 1 == await r.ts().add("time-serie-add-ooo-block", 1, 5.0) with pytest.raises(Exception): - await r.ts().add("time-serie-add-ooo-block", 1, 5.0, duplicate_policy="block") + await r.ts().add("time-serie-add-ooo-block", 1, 5.0, on_duplicate="block") # Test for duplicate policy LAST assert 1 == await r.ts().add("time-serie-add-ooo-last", 1, 5.0) assert 1 == await r.ts().add( - "time-serie-add-ooo-last", 1, 10.0, duplicate_policy="last" + "time-serie-add-ooo-last", 1, 10.0, on_duplicate="last" ) res = await r.ts().get("time-serie-add-ooo-last") assert 10.0 == res[1] @@ -130,24 +130,20 @@ async def test_add_duplicate_policy(r: redis.Redis): # Test for duplicate policy FIRST assert 1 == await r.ts().add("time-serie-add-ooo-first", 1, 5.0) assert 1 == await r.ts().add( - "time-serie-add-ooo-first", 1, 10.0, duplicate_policy="first" + "time-serie-add-ooo-first", 1, 10.0, on_duplicate="first" ) res = await r.ts().get("time-serie-add-ooo-first") assert 5.0 == res[1] # Test for duplicate policy MAX assert 1 == await r.ts().add("time-serie-add-ooo-max", 1, 5.0) - assert 1 == await r.ts().add( - "time-serie-add-ooo-max", 1, 10.0, duplicate_policy="max" - ) + assert 1 == await r.ts().add("time-serie-add-ooo-max", 1, 10.0, on_duplicate="max") res = await r.ts().get("time-serie-add-ooo-max") assert 10.0 == res[1] # Test for duplicate policy MIN assert 1 == await r.ts().add("time-serie-add-ooo-min", 1, 5.0) - assert 1 == await r.ts().add( - "time-serie-add-ooo-min", 1, 10.0, duplicate_policy="min" - ) + assert 1 == await r.ts().add("time-serie-add-ooo-min", 1, 10.0, on_duplicate="min") res = await r.ts().get("time-serie-add-ooo-min") assert 5.0 == res[1] @@ -214,7 +210,7 @@ async def test_create_and_delete_rule(decoded_r: redis.Redis): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") async def test_del_range(decoded_r: redis.Redis): try: await decoded_r.ts().delete("test", 0, 100) @@ -248,7 +244,7 @@ async def test_range(decoded_r: redis.Redis): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") async def test_range_advanced(decoded_r: redis.Redis): for i in range(100): await decoded_r.ts().add(1, i, i % 7) @@ -279,7 +275,7 @@ async def test_range_advanced(decoded_r: redis.Redis): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") async def test_rev_range(decoded_r: redis.Redis): for i in range(100): await decoded_r.ts().add(1, i, i % 7) @@ -379,7 +375,7 @@ async def test_multi_range(decoded_r: redis.Redis): @pytest.mark.onlynoncluster @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") async def test_multi_range_advanced(decoded_r: redis.Redis): await decoded_r.ts().create(1, labels={"Test": "This", "team": "ny"}) await decoded_r.ts().create( @@ -497,7 +493,7 @@ async def test_multi_range_advanced(decoded_r: redis.Redis): @pytest.mark.onlynoncluster @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") async def test_multi_reverse_range(decoded_r: redis.Redis): await decoded_r.ts().create(1, labels={"Test": "This", "team": "ny"}) await decoded_r.ts().create( @@ -752,9 +748,117 @@ async def test_query_index(decoded_r: redis.Redis): async def test_uncompressed(decoded_r: redis.Redis): await decoded_r.ts().create("compressed") await decoded_r.ts().create("uncompressed", uncompressed=True) + for i in range(1000): + await decoded_r.ts().add("compressed", i, i) + await decoded_r.ts().add("uncompressed", i, i) compressed_info = await decoded_r.ts().info("compressed") uncompressed_info = await decoded_r.ts().info("uncompressed") if is_resp2_connection(decoded_r): assert compressed_info.memory_usage != uncompressed_info.memory_usage else: assert compressed_info["memoryUsage"] != uncompressed_info["memoryUsage"] + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +async def test_create_with_insertion_filters(decoded_r: redis.Redis): + await decoded_r.ts().create( + "time-series-1", + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + assert 1000 == await decoded_r.ts().add("time-series-1", 1000, 1.0) + assert 1010 == await decoded_r.ts().add("time-series-1", 1010, 11.0) + assert 1010 == await decoded_r.ts().add("time-series-1", 1013, 10.0) + assert 1020 == await decoded_r.ts().add("time-series-1", 1020, 11.5) + assert 1021 == await decoded_r.ts().add("time-series-1", 1021, 22.0) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 1.0), (1010, 11.0), (1020, 11.5), (1021, 22.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +async def test_alter_with_insertion_filters(decoded_r: redis.Redis): + assert 1000 == await decoded_r.ts().add("time-series-1", 1000, 1.0) + assert 1010 == await decoded_r.ts().add("time-series-1", 1010, 11.0) + assert 1013 == await decoded_r.ts().add("time-series-1", 1013, 10.0) + + await decoded_r.ts().alter( + "time-series-1", + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1013 == await decoded_r.ts().add("time-series-1", 1015, 11.5) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 1.0), (1010, 11.0), (1013, 10.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +async def test_add_with_insertion_filters(decoded_r: redis.Redis): + assert 1000 == await decoded_r.ts().add( + "time-series-1", + 1000, + 1.0, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1000 == await decoded_r.ts().add("time-series-1", 1004, 3.0) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 1.0)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +async def test_incrby_with_insertion_filters(decoded_r: redis.Redis): + assert 1000 == await decoded_r.ts().incrby( + "time-series-1", + 1.0, + timestamp=1000, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1000 == await decoded_r.ts().incrby("time-series-1", 3.0, timestamp=1000) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 1.0)] + assert expected_points == data_points + + assert 1000 == await decoded_r.ts().incrby("time-series-1", 10.1, timestamp=1000) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, 11.1)] + assert expected_points == data_points + + +@skip_ifmodversion_lt("1.12.0", "timeseries") +async def test_decrby_with_insertion_filters(decoded_r: redis.Redis): + assert 1000 == await decoded_r.ts().decrby( + "time-series-1", + 1.0, + timestamp=1000, + duplicate_policy="last", + ignore_max_time_diff=5, + ignore_max_val_diff=10.0, + ) + + assert 1000 == await decoded_r.ts().decrby("time-series-1", 3.0, timestamp=1000) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, -1.0)] + assert expected_points == data_points + + assert 1000 == await decoded_r.ts().decrby("time-series-1", 10.1, timestamp=1000) + + data_points = await decoded_r.ts().range("time-series-1", "-", "+") + expected_points = [(1000, -11.1)] + assert expected_points == data_points diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 0cf16de993..5647bd45c6 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -84,7 +84,7 @@ def test_alter(client): @pytest.mark.redismod @skip_ifmodversion_lt("1.4.0", "timeseries") -def test_alter_diplicate_policy(client): +def test_alter_duplicate_policy(client): assert client.ts().create(1) info = client.ts().info(1) assert_resp_response( @@ -126,7 +126,7 @@ def test_add_on_duplicate(client): # Test for duplicate policy BLOCK assert 1 == client.ts().add("time-serie-add-ooo-block", 1, 5.0) with pytest.raises(Exception): - client.ts().add("time-serie-add-ooo-block", 1, 5.0, duplicate_policy="block") + client.ts().add("time-serie-add-ooo-block", 1, 5.0, on_duplicate="block") # Test for duplicate policy LAST assert 1 == client.ts().add("time-serie-add-ooo-last", 1, 5.0) @@ -220,7 +220,7 @@ def test_create_and_delete_rule(client): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") def test_del_range(client): try: client.ts().delete("test", 0, 100) @@ -250,7 +250,7 @@ def test_range(client): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") def test_range_advanced(client): for i in range(100): client.ts().add(1, i, i % 7) @@ -384,7 +384,7 @@ def test_range_empty(client: redis.Redis): @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") def test_rev_range(client): for i in range(100): client.ts().add(1, i, i % 7) @@ -581,7 +581,7 @@ def test_mrange(client): @pytest.mark.onlynoncluster @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") def test_multi_range_advanced(client): client.ts().create(1, labels={"Test": "This", "team": "ny"}) client.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) @@ -725,7 +725,7 @@ def test_mrange_latest(client: redis.Redis): @pytest.mark.onlynoncluster @pytest.mark.redismod -@skip_ifmodversion_lt("99.99.99", "timeseries") +@skip_ifmodversion_lt("1.10.0", "timeseries") def test_multi_reverse_range(client): client.ts().create(1, labels={"Test": "This", "team": "ny"}) client.ts().create(2, labels={"Test": "This", "Taste": "That", "team": "sf"}) From 48e55ad41df5ac3bde5d5afe5e10cb7035812881 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 12 Jun 2024 11:21:18 +0300 Subject: [PATCH 09/10] Fix discovery of module info in tests --- tests/conftest.py | 6 ++++- tests/test_asyncio/test_timeseries.py | 36 ++++++++++++++++----------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9263c4353d..6df6875845 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,8 +157,12 @@ def pytest_sessionstart(session): session.config.REDIS_INFO = REDIS_INFO # module info + stack_url = redis_url + if stack_url == default_redis_url: + stack_url = default_redismod_url try: - REDIS_INFO["modules"] = info["modules"] + stack_info = _get_info(stack_url) + REDIS_INFO["modules"] = stack_info["modules"] except (KeyError, redis.exceptions.ConnectionError): pass diff --git a/tests/test_asyncio/test_timeseries.py b/tests/test_asyncio/test_timeseries.py index 93298bfa90..c93af1ea5b 100644 --- a/tests/test_asyncio/test_timeseries.py +++ b/tests/test_asyncio/test_timeseries.py @@ -113,38 +113,44 @@ async def test_add(decoded_r: redis.Redis): @pytest.mark.redismod @skip_ifmodversion_lt("1.4.0", "timeseries") -async def test_add_duplicate_policy(r: redis.Redis): +async def test_add_duplicate_policy(decoded_r: redis.Redis): # Test for duplicate policy BLOCK - assert 1 == await r.ts().add("time-serie-add-ooo-block", 1, 5.0) + assert 1 == await decoded_r.ts().add("time-serie-add-ooo-block", 1, 5.0) with pytest.raises(Exception): - await r.ts().add("time-serie-add-ooo-block", 1, 5.0, on_duplicate="block") + await decoded_r.ts().add( + "time-serie-add-ooo-block", 1, 5.0, on_duplicate="block" + ) # Test for duplicate policy LAST - assert 1 == await r.ts().add("time-serie-add-ooo-last", 1, 5.0) - assert 1 == await r.ts().add( + assert 1 == await decoded_r.ts().add("time-serie-add-ooo-last", 1, 5.0) + assert 1 == await decoded_r.ts().add( "time-serie-add-ooo-last", 1, 10.0, on_duplicate="last" ) - res = await r.ts().get("time-serie-add-ooo-last") + res = await decoded_r.ts().get("time-serie-add-ooo-last") assert 10.0 == res[1] # Test for duplicate policy FIRST - assert 1 == await r.ts().add("time-serie-add-ooo-first", 1, 5.0) - assert 1 == await r.ts().add( + assert 1 == await decoded_r.ts().add("time-serie-add-ooo-first", 1, 5.0) + assert 1 == await decoded_r.ts().add( "time-serie-add-ooo-first", 1, 10.0, on_duplicate="first" ) - res = await r.ts().get("time-serie-add-ooo-first") + res = await decoded_r.ts().get("time-serie-add-ooo-first") assert 5.0 == res[1] # Test for duplicate policy MAX - assert 1 == await r.ts().add("time-serie-add-ooo-max", 1, 5.0) - assert 1 == await r.ts().add("time-serie-add-ooo-max", 1, 10.0, on_duplicate="max") - res = await r.ts().get("time-serie-add-ooo-max") + assert 1 == await decoded_r.ts().add("time-serie-add-ooo-max", 1, 5.0) + assert 1 == await decoded_r.ts().add( + "time-serie-add-ooo-max", 1, 10.0, on_duplicate="max" + ) + res = await decoded_r.ts().get("time-serie-add-ooo-max") assert 10.0 == res[1] # Test for duplicate policy MIN - assert 1 == await r.ts().add("time-serie-add-ooo-min", 1, 5.0) - assert 1 == await r.ts().add("time-serie-add-ooo-min", 1, 10.0, on_duplicate="min") - res = await r.ts().get("time-serie-add-ooo-min") + assert 1 == await decoded_r.ts().add("time-serie-add-ooo-min", 1, 5.0) + assert 1 == await decoded_r.ts().add( + "time-serie-add-ooo-min", 1, 10.0, on_duplicate="min" + ) + res = await decoded_r.ts().get("time-serie-add-ooo-min") assert 5.0 == res[1] From 808f592e8d771879a51984c7b11d2b3b907aaa25 Mon Sep 17 00:00:00 2001 From: Gabriel Erzse Date: Wed, 12 Jun 2024 11:48:34 +0300 Subject: [PATCH 10/10] Fix env in CI --- .github/workflows/integration.yaml | 162 ++++++++++++++--------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index a7a5513c64..11b08c934e 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -23,90 +23,91 @@ concurrency: permissions: contents: read # to fetch code (actions/checkout) -jobs: +env: + REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc1 - dependency-audit: - name: Dependency audit - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: pypa/gh-action-pip-audit@v1.0.8 - with: - inputs: requirements.txt dev_requirements.txt - ignore-vulns: | - GHSA-w596-4wvx-j9j6 # subversion related git pull, dependency for pytest. There is no impact here. +jobs: + dependency-audit: + name: Dependency audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pypa/gh-action-pip-audit@v1.0.8 + with: + inputs: requirements.txt dev_requirements.txt + ignore-vulns: | + GHSA-w596-4wvx-j9j6 # subversion related git pull, dependency for pytest. There is no impact here. - lint: - name: Code linters - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.9 - cache: 'pip' - - name: run code linters - run: | - pip install -r dev_requirements.txt - invoke linters + lint: + name: Code linters + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.9 + cache: 'pip' + - name: run code linters + run: | + pip install -r dev_requirements.txt + invoke linters - run-tests: - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - max-parallel: 15 - fail-fast: false - matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.8', 'pypy-3.9'] - test-type: ['standalone', 'cluster'] - connection-type: ['hiredis', 'plain'] - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc1 - name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: run tests - run: | - pip install -U setuptools wheel - pip install -r requirements.txt - pip install -r dev_requirements.txt - if [ "${{matrix.connection-type}}" == "hiredis" ]; then - pip install hiredis - fi - invoke devenv - sleep 10 # time to settle - invoke ${{matrix.test-type}}-tests + run-tests: + runs-on: ubuntu-latest + timeout-minutes: 60 + strategy: + max-parallel: 15 + fail-fast: false + matrix: + python-version: ['3.8', '3.9', '3.10', '3.11', 'pypy-3.8', 'pypy-3.9'] + test-type: ['standalone', 'cluster'] + connection-type: ['hiredis', 'plain'] + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + name: Python ${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}} tests + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: run tests + run: | + pip install -U setuptools wheel + pip install -r requirements.txt + pip install -r dev_requirements.txt + if [ "${{matrix.connection-type}}" == "hiredis" ]; then + pip install hiredis + fi + invoke devenv + sleep 10 # time to settle + invoke ${{matrix.test-type}}-tests - - uses: actions/upload-artifact@v4 - if: success() || failure() - with: - name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}} - path: '${{matrix.test-type}}*results.xml' + - uses: actions/upload-artifact@v4 + if: success() || failure() + with: + name: pytest-results-${{matrix.test-type}}-${{matrix.connection-type}}-${{matrix.python-version}} + path: '${{matrix.test-type}}*results.xml' - - name: Upload codecov coverage - uses: codecov/codecov-action@v4 - with: - fail_ci_if_error: false + - name: Upload codecov coverage + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: false - - name: View Test Results - uses: dorny/test-reporter@v1 - if: success() || failure() - continue-on-error: true - with: - name: Test Results ${{matrix.python-version}} ${{matrix.test-type}}-${{matrix.connection-type}} - path: '*.xml' - reporter: java-junit - list-suites: all - list-tests: all - max-annotations: 10 - fail-on-error: 'false' + - name: View Test Results + uses: dorny/test-reporter@v1 + if: success() || failure() + continue-on-error: true + with: + name: Test Results ${{matrix.python-version}} ${{matrix.test-type}}-${{matrix.connection-type}} + path: '*.xml' + reporter: java-junit + list-suites: all + list-tests: all + max-annotations: 10 + fail-on-error: 'false' - resp3_tests: + resp3_tests: runs-on: ubuntu-latest strategy: fail-fast: false @@ -116,8 +117,7 @@ jobs: connection-type: ['hiredis', 'plain'] protocol: ['3'] env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: true - REDIS_STACK_IMAGE: redis/redis-stack-server:7.4.0-rc1 + ACTIONS_ALLOW_UNSECURE_COMMANDS: true name: RESP3 [${{ matrix.python-version }} ${{matrix.test-type}}-${{matrix.connection-type}}] steps: - uses: actions/checkout@v4 @@ -138,7 +138,7 @@ jobs: invoke ${{matrix.test-type}}-tests invoke ${{matrix.test-type}}-tests --uvloop - build_and_test_package: + build_and_test_package: name: Validate building and installing the package runs-on: ubuntu-latest needs: [run-tests] @@ -155,7 +155,7 @@ jobs: run: | bash .github/workflows/install_and_test.sh ${{ matrix.extension }} - install_package_from_commit: + install_package_from_commit: name: Install package from commit hash runs-on: ubuntu-latest strategy: