Skip to content

Commit 4a51661

Browse files
committed
Updated measurement/series related tests.
1 parent 396bad7 commit 4a51661

File tree

4 files changed

+156
-46
lines changed

4 files changed

+156
-46
lines changed

c8y_api/model/measurements.py

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -716,19 +716,11 @@ def get_series(
716716
717717
See also: https://cumulocity.com/api/core/#operation/getMeasurementSeriesResource
718718
"""
719-
# The 'series' parameter has to be added through a hack; it
720-
# may be a list and because 'series' is by default converted to
721-
# the 'valueFragmentSeries' parameter
722-
723-
# if series:
724-
# series = series if isinstance(series, str) else ','.join(series)
725-
726719
base_query = self._prepare_query(
727720
resource=f'{self.resource}/series',
728721
expression=expression,
729722
source=source,
730-
# this is a non-mapped parameter
731-
aggregationType=aggregation,
723+
aggregationType=aggregation, # this is a non-mapped parameter
732724
series=series,
733725
before=before,
734726
after=after,
@@ -738,14 +730,81 @@ def get_series(
738730
**kwargs)
739731
return Series(self.c8y.get(base_query))
740732

733+
def collect_series(
734+
self,
735+
expression: str = None,
736+
source: str = None,
737+
aggregation: str = None,
738+
series: str | Sequence[str] = None,
739+
before: str | datetime = None,
740+
after: str | datetime = None,
741+
min_age: timedelta = None,
742+
max_age: timedelta = None,
743+
reverse: bool = None,
744+
value: str = None,
745+
timestamps: bool|str = None,
746+
**kwargs
747+
):
748+
"""Query the database for series values.
749+
750+
This function is functionally the same as using the `get_series` function
751+
with an immediate `collect` on the result.
752+
753+
Args:
754+
expression (str): Arbitrary filter expression which will be
755+
passed to Cumulocity without change; all other filters
756+
are ignored if this is provided
757+
source (str): Database ID of a source device
758+
aggregation (str): Aggregation type
759+
series (str|Sequence[str]): Series' to query and collect; If
760+
multiple series are collected each element in the result will
761+
be a tuple. If omitted, all available series are collected.
762+
before (datetime|str): Datetime object or ISO date/time string.
763+
Only measurements assigned to a time before this date are
764+
included.
765+
after (datetime|str): Datetime object or ISO date/time string.
766+
Only measurements assigned to a time after this date are
767+
included.
768+
min_age (timedelta): Timedelta object. Only measurements of
769+
at least this age are included.
770+
max_age (timedelta): Timedelta object. Only measurements with
771+
at most this age are included.
772+
reverse (bool): Invert the order of results, starting with the
773+
most recent one.
774+
value (str): Which value (min/max) to collect. If omitted, both
775+
values will be collected, grouped as 2-tuples.
776+
timestamps (bool|str): Whether each element in the result list will
777+
be prepended with the corresponding timestamp. If True, the
778+
timestamp string will be included; Use 'datetime' or 'epoch' to
779+
parse the timestamp string.
780+
781+
Returns:
782+
A simple list or list of tuples (potentially nested) depending on the
783+
parameter combination.
784+
785+
See also: https://cumulocity.com/api/core/#operation/getMeasurementSeriesResource
786+
"""
787+
result = self.get_series(
788+
expression=expression,
789+
source=source,
790+
aggregation=aggregation,
791+
series=series,
792+
before=before,
793+
after=after,
794+
min_age=min_age,
795+
max_age=max_age,
796+
reverse=reverse,
797+
**kwargs)
798+
return result.collect(
799+
series=series,
800+
value=value,
801+
timestamps=timestamps)
802+
741803
def delete_by(
742804
self,
743805
expression: str = None,
744806
type: str = None,
745807
source: str | int = None,
746-
# value_fragment_type: str = None,
747-
# value_fragment_series: str = None,
748-
# series: str = None,
749808
date_from: str | datetime = None,
750809
date_to: str | datetime = None,
751810
before: str | datetime = None,
@@ -785,9 +844,6 @@ def delete_by(
785844
expression=expression,
786845
type=type,
787846
source=source,
788-
# value_fragment_type=value_fragment_type,
789-
# value_fragment_series=value_fragment_series,
790-
# series=series,
791847
date_from=date_from,
792848
date_to=date_to,
793849
before=before,

integration_tests/test_measurements.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,6 @@ def unaggregated_series_result(live_c8y: CumulocityApi, sample_series_device: De
216216
def aggregated_series_result(live_c8y: CumulocityApi, sample_series_device: Device) -> Series:
217217
"""Provide an aggregated series result."""
218218
start_time = datetime.fromisoformat('2020-01-01 00:00:00+00:00')
219-
print(f"Type: {type(sample_series_device.c8y_SupportedSeries)}")
220-
print(f"Values: {sample_series_device.c8y_SupportedSeries}")
221219
return live_c8y.measurements.get_series(source=sample_series_device.id,
222220
series=sample_series_device.c8y_SupportedSeries,
223221
aggregation=Measurements.AggregationType.HOURLY,
@@ -263,3 +261,37 @@ def test_collect_multiple_series(series_fixture, request):
263261
for i in range(0, len(series_names)):
264262
t = type(values[0][i])
265263
assert all(isinstance(v[i], t) for v in values if v[i])
264+
265+
266+
def test_get_and_collect_series(live_c8y, sample_series_device):
267+
"""Verify that get & collect works as expected."""
268+
series = live_c8y.measurements.get_series(
269+
source=sample_series_device.id,
270+
series=sample_series_device.c8y_SupportedSeries,
271+
aggregation=Measurements.AggregationType.HOURLY,
272+
after='1970-01-01',
273+
before='now'
274+
)
275+
276+
# multiple series
277+
collected = series.collect(sample_series_device.c8y_SupportedSeries)
278+
directly_collected = live_c8y.measurements.collect_series(
279+
source=sample_series_device.id,
280+
series=sample_series_device.c8y_SupportedSeries,
281+
aggregation=Measurements.AggregationType.HOURLY,
282+
after='1970-01-01',
283+
before='now'
284+
)
285+
assert collected == directly_collected
286+
287+
# single series
288+
for series_name in sample_series_device.c8y_SupportedSeries:
289+
collected = series.collect(series_name)
290+
directly_collected = live_c8y.measurements.collect_series(
291+
source=sample_series_device.id,
292+
series=series_name,
293+
aggregation=Measurements.AggregationType.HOURLY,
294+
after='1970-01-01',
295+
before='now'
296+
)
297+
assert collected == directly_collected

tests/model/test_base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ def test_complex_object_updating(fun):
344344

345345

346346
def test_inheritance():
347+
"""Verify that the base classes inheritance and typing is as expected."""
348+
# pylint: disable=unidiomatic-typecheck
347349
mo = ManagedObject.from_json({
348350
'id': 'ID',
349351
'name': 'NAME',

tests/model/test_measurements.py

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -127,32 +127,6 @@ def test_select_invalid_combinations(fun, args, errors):
127127
isolate_call_url(fun, **params)
128128
assert all(e in str(error) for e in errors)
129129

130-
131-
def generate_series_data() -> tuple:
132-
"""Generate all kinds of combinations of series fragments."""
133-
134-
def gen_level2(n):
135-
level2_single = ({n: {'1': {'value': 1}}}, [f'{n}.1'])
136-
level2_multi = ({n: {'1': {'value': 1}, '2': {'value': 2}}}, [f'{n}.1', f'{n}.2'])
137-
level2_invalid = ({n: {'1': {'data': 1}}}, [])
138-
level2_mix1 = ({n: {'1': {'data': 1}, '2': {'value': 2}}}, [f'{n}.2'])
139-
level2_mix2 = ({n: {'1': {'value': 1}, '2': {'data': 2}}}, [f'{n}.1'])
140-
141-
return [level2_single, level2_multi, level2_invalid, level2_mix1, level2_mix2]
142-
143-
a = gen_level2('A')
144-
b = gen_level2('B')
145-
# collecting combinations of A and B cases
146-
# (first element is the json, 2nd the expectation for each combination)
147-
ab = [({**r[0][0], **r[1][0]}, r[0][1] + r[1][1]) for r in itertools.product(a, b)]
148-
149-
cases = a + ab
150-
# id is the beautified expectation, prefixed with a number
151-
ids = [f'{i}: ' + ','.join(map(lambda x: x.replace('.', ''), x[1])) for i, x in enumerate(cases)]
152-
153-
return cases, ids
154-
155-
156130
@pytest.mark.parametrize('params, expected, not_expected', [
157131
({'expression': 'X&Y'}, ['X&Y'], ['expression']),
158132
({'source': 'SOURCE'}, ['source=SOURCE'], []),
@@ -172,13 +146,59 @@ def test_get_series_parameters(params, expected, not_expected):
172146
assert e not in resource
173147

174148

175-
@pytest.mark.skip("TODO: test purpose unclear")
149+
def generate_series_data() -> tuple:
150+
"""Generate all kinds of combinations of series fragments.
151+
152+
Returns a tuple of testcases and corresponding testcase ID. Each testcase
153+
element is again a tuple of a JSON structure (the test data) and a list
154+
of expected series' names (for assertion).
155+
156+
We will define 2 sets (A and B) of such test cases (with different fragment
157+
names), each featuring possible JSON combinations of single and multiple
158+
series as well as invalid structures (not following the syntax for series).
159+
160+
Finally, we will create test cases from all possible combinations of the two
161+
basic sets.
162+
163+
The tests' ID are generated from the expectation set.
164+
"""
165+
166+
def generate(fragment):
167+
level2_single = ({fragment: {'series1': {'value': 1}}},
168+
[f'{fragment}.series1'])
169+
level2_multi = ({fragment: {'series1': {'value': 1}, 'series2': {'value': 2}}},
170+
[f'{fragment}.series1', f'{fragment}.series2'])
171+
level2_invalid = ({fragment: {'series1': {'data': 1}}},
172+
[])
173+
level2_mix1 = ({fragment: {'series1': {'data': 1}, 'series2': {'value': 2}}},
174+
[f'{fragment}.series2'])
175+
level2_mix2 = ({fragment: {'series1': {'value': 1}, 'series2': {'data': 2}}},
176+
[f'{fragment}.series1'])
177+
return [level2_single, level2_multi, level2_invalid, level2_mix1, level2_mix2]
178+
179+
# generating A and B sets
180+
a = generate('fragmentA')
181+
b = generate('fragmentB')
182+
183+
# collecting combinations of A and B cases
184+
ab = [({**r[0][0], **r[1][0]}, r[0][1] + r[1][1]) for r in itertools.product(a, b)]
185+
186+
cases = a + ab
187+
# id is the beautified expectation, prefixed with a number
188+
ids = [f'{i}: ' + ','.join(map(lambda x: x.replace('.', '/'), x[1])) for i, x in enumerate(cases)]
189+
190+
return cases, ids
191+
192+
176193
@pytest.mark.parametrize('testcase', generate_series_data()[0], ids=generate_series_data()[1])
177194
def test_get_series(testcase):
178-
"""Verify that the get_series function works as expected."""
195+
"""Verify that the get_series function works as expected.
196+
197+
The `get_series` function on a measurement determines and returns the
198+
names of series defined within a single measurement.
199+
"""
179200
data = {**testcase[0], 'source': {'id': '1'}}
180201
m = Measurement.from_json(data)
181-
# expected = [*xs for xs in testcase[1]]
182202
assert testcase[1] == m.get_series()
183203

184204

0 commit comments

Comments
 (0)