Skip to content

Commit 7a88801

Browse files
committed
Using lowercase boolean values; Adding references to get function of inventory API.
1 parent d9e3db4 commit 7a88801

File tree

13 files changed

+241
-42
lines changed

13 files changed

+241
-42
lines changed

c8y_api/model/_base.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ def _map_params(
685685
def multi(*xs):
686686
return sum(bool(x) for x in xs) > 1
687687

688+
def stringify(value):
689+
if isinstance(value, bool):
690+
return str(value).lower()
691+
return value
692+
688693
if multi(min_age, before, date_to):
689694
raise ValueError("Only one of 'min_age', 'before' and 'date_to' query parameters must be used.")
690695
if multi(max_age, after, date_from):
@@ -739,11 +744,11 @@ def multi(*xs):
739744
'createdTo': created_to,
740745
'lastUpdatedFrom': updated_from,
741746
'lastUpdatedTo': updated_to,
742-
'withSourceAssets': with_source_assets,
743-
'withSourceDevices': with_source_devices,
744-
'revert': str(reverse).lower() if reverse is not None else None,
747+
'withSourceAssets': stringify(with_source_assets),
748+
'withSourceDevices': stringify(with_source_devices),
749+
'revert': stringify(reverse),
745750
'pageSize': page_size}.items() if v is not None}
746-
params.update({_StringUtil.to_pascal_case(k): v for k, v in kwargs.items() if v is not None})
751+
params.update({_StringUtil.to_pascal_case(k): stringify(v) for k, v in kwargs.items() if v is not None})
747752
tuples = list(params.items())
748753
if series:
749754
if isinstance(series, list):
@@ -758,8 +763,9 @@ def _prepare_query(self, resource: str = None, expression: str = None, **kwargs)
758763
return resource or self.resource
759764
return (resource or self.resource) + '?' + encoded
760765

761-
def _get_object(self, object_id):
762-
return self.c8y.get(self.build_object_path(object_id))
766+
def _get_object(self, object_id, **kwargs):
767+
query = self._prepare_query(self.build_object_path(object_id), **kwargs)
768+
return self.c8y.get(query)
763769

764770
def _get_page(self, base_query: str, page_number: int):
765771
sep = '&' if '?' in base_query else '?'

c8y_api/model/inventory.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,46 @@ class Inventory(CumulocityResource):
2323
def __init__(self, c8y):
2424
super().__init__(c8y, 'inventory/managedObjects')
2525

26-
def get(self, id) -> ManagedObject: # noqa (id)
26+
def get(
27+
self,
28+
id: str, # noqa
29+
with_children: bool = None,
30+
with_children_count: bool = None,
31+
skip_children_names: bool = None,
32+
with_parents: bool = None,
33+
with_latest_values: bool = None,
34+
**kwargs) -> ManagedObject:
2735
""" Retrieve a specific managed object from the database.
2836
2937
Args:
30-
ID of the managed object
38+
id (str): Cumulocity ID of the managed object
39+
with_children (bool): Whether children with ID and name should be
40+
included with each returned object
41+
with_children_count (bool): When set to true, the returned result
42+
will contain the total number of children in the respective
43+
child additions, assets and devices sub fragments.
44+
skip_children_names (bool): If true, returned references of child
45+
devices won't contain their names.
46+
with_parents (bool): Whether to include a device's parents.
47+
with_latest_values (bool): If true the platform includes the
48+
fragment `c8y_LatestMeasurements, which contains the latest
49+
measurement values reported by the device to the platform.
3150
3251
Returns:
3352
A ManagedObject instance
3453
3554
Raises:
3655
KeyError: if the ID is not defined within the database
3756
"""
38-
managed_object = ManagedObject.from_json(self._get_object(id))
57+
managed_object = ManagedObject.from_json(self._get_object(
58+
id,
59+
with_children=with_children,
60+
with_children_count=with_children_count,
61+
skip_children_names=skip_children_names,
62+
with_parents=with_parents,
63+
with_latest_values=with_latest_values,
64+
**kwargs)
65+
)
3966
managed_object.c8y = self.c8y # inject c8y connection into instance
4067
return managed_object
4168

@@ -155,7 +182,8 @@ def get_by(
155182
**kwargs))
156183
if len(result) == 1:
157184
return result[0]
158-
raise ValueError("No matching object found." if not result else "Ambiguous query; multiple matching objects found.")
185+
raise ValueError("No matching object found." if not result
186+
else "Ambiguous query; multiple matching objects found.")
159187

160188
def get_count(
161189
self,
@@ -514,19 +542,46 @@ def accept(self, id: str): # noqa (id)
514542
"""
515543
self.c8y.put('/devicecontrol/newDeviceRequests/' + str(id), {'status': 'ACCEPTED'})
516544

517-
def get(self, id: str) -> Device: # noqa (id)
545+
def get(
546+
self,
547+
id: str, # noqa
548+
with_children: bool = None,
549+
with_children_count: bool = None,
550+
skip_children_names: bool = None,
551+
with_parents: bool = None,
552+
with_latest_values: bool = None,
553+
**kwargs) -> Device:
518554
""" Retrieve a specific device object.
519555
520556
Args:
521-
id (str): ID of the device object
557+
id (str): Cumulocity ID of the device object
558+
with_children (bool): Whether children with ID and name should be
559+
included with each returned object
560+
with_children_count (bool): When set to true, the returned result
561+
will contain the total number of children in the respective
562+
child additions, assets and devices sub fragments.
563+
skip_children_names (bool): If true, returned references of child
564+
devices won't contain their names.
565+
with_parents (bool): Whether to include a device's parents.
566+
with_latest_values (bool): If true the platform includes the
567+
fragment `c8y_LatestMeasurements, which contains the latest
568+
measurement values reported by the device to the platform.
522569
523570
Returns:
524571
A Device instance
525572
526573
Raises:
527574
KeyError: if the ID is not defined within the database
528575
"""
529-
device = Device.from_json(self._get_object(id))
576+
device = Device.from_json(self._get_object(
577+
id,
578+
with_children=with_children,
579+
with_children_count=with_children_count,
580+
skip_children_names=skip_children_names,
581+
with_parents=with_parents,
582+
with_latest_values=with_latest_values,
583+
**kwargs)
584+
)
530585
device.c8y = self.c8y
531586
return device
532587

c8y_api/model/managedobjects.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ def __init__(self, c8y: CumulocityRestApi = None,
286286
self.update_time = None
287287
self.child_devices = []
288288
self.child_assets = []
289-
"""List of NamedObject references to child assets."""
290289
self.child_additions = []
291290
self.parent_devices = []
292291
self.parent_assets = []
@@ -331,12 +330,16 @@ def _from_json(cls, json: dict, obj: Any) -> Any:
331330
mo.is_device = True
332331
if 'c8y_IsBinary' in json:
333332
mo.is_binary = True
334-
if 'childDevices' in json:
335-
mo.child_devices = cls._parse_references(json['childDevices'])
336-
if 'childAssets' in json:
337-
mo.child_assets = cls._parse_references(json['childAssets'])
338-
if 'childAdditions' in json:
339-
mo.child_additions = cls._parse_references(json['childAdditions'])
333+
for fragment, field in [
334+
('childDevices', 'child_devices'),
335+
('childAssets', 'child_assets'),
336+
('childAdditions', 'child_additions'),
337+
('deviceParents', 'parent_devices'),
338+
('assetParents', 'parent_assets'),
339+
('additionParents', 'parent_additions'),
340+
]:
341+
if fragment in json:
342+
mo.__dict__[field] = cls._parse_references(json[fragment])
340343
return mo
341344

342345
@classmethod

c8y_tk/app/subscription_listener.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ def listen(self):
119119
120120
This is blocking.
121121
"""
122+
# pylint: disable=too-many-branches
123+
122124
# safely invoke a callback function blocking or non-blocking
123125
def invoke_callback(callback, is_blocking, _, arg):
124126
def safe_invoke(a):
@@ -128,7 +130,7 @@ def safe_invoke(a):
128130
self._log.debug(f"Invoking callback: {callback.__module__}.{callback.__name__}")
129131
callback(a)
130132
except Exception as callback_error:
131-
self._log.error(f"Uncaught exception in callback: {callback_error}", exc_info=error)
133+
self._log.error(f"Uncaught exception in callback: {callback_error}", exc_info=callback_error)
132134
if is_blocking:
133135
safe_invoke(arg)
134136
else:
@@ -180,8 +182,8 @@ def safe_invoke(a):
180182
# schedule next run, skip if already exceeded
181183
next_run = time.monotonic() + self.polling_interval
182184
if self._log.isEnabledFor(logging.DEBUG):
183-
next_run_datetime = (datetime.now(timezone.utc) + timedelta(seconds=self.polling_interval) ).isoformat(sep=' ', timespec='seconds')
184-
self._log.debug(f"Next run at {next_run_datetime}.")
185+
next_run_datetime = datetime.now(timezone.utc) + timedelta(seconds=self.polling_interval)
186+
self._log.debug(f"Next run at {next_run_datetime.isoformat(sep=' ', timespec='seconds')}.")
185187
# sleep until next poll
186188
if not time.monotonic() > next_run:
187189
self._is_closed.wait(next_run - time.monotonic())
@@ -192,6 +194,7 @@ def safe_invoke(a):
192194
if self._executor:
193195
self._executor.shutdown(wait=False, cancel_futures=False)
194196

197+
# pylint: disable=broad-exception-caught
195198
except Exception as error:
196199
self._log.error(f"Uncaught exception during listen: {error}", exc_info=error)
197200

integration_tests/test_alarms.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ def sample_alarms(session_device, module_factory) -> List[Alarm]:
141141
]
142142

143143

144-
145144
def test_apply_by(live_c8y: CumulocityApi, session_device: Device):
146145
"""Verify that the apply_by function works."""
147146

integration_tests/test_inventory.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,99 @@ def test_reload(live_c8y):
170170
assert 'c8y_AdditionalFragment' not in obj2.fragments
171171

172172

173+
@pytest.fixture(name='asset_hierarchy_root_id', scope='module')
174+
def fix_asset_hierarchy_root_id(module_factory):
175+
"""Provide a (read-only) sample asset hierarchy for corresponding tests.
176+
177+
This fixture creates a root object with a child of each kind (asset,
178+
device, addition). Each of the children references to another 'addition'
179+
child to create a multi-level hierarchy.
180+
181+
It is automatically cleaned up after testing.
182+
"""
183+
name = RandomNameGenerator.random_name()
184+
obj = module_factory(ManagedObject(name=f'Root-{name}', type=f'Root-{name}'))
185+
186+
addition = module_factory(ManagedObject(name=f'Addition-{name}', type=f'Addition-{name}'))
187+
asset = module_factory(ManagedObject(name=f'Asset-{name}', type=f'Asset-{name}'))
188+
device = module_factory(Device(name=f'Device-{name}', type=f'Device-{name}'))
189+
obj.add_child_addition(addition)
190+
obj.add_child_asset(asset)
191+
obj.add_child_device(device)
192+
193+
sub_addition = module_factory(ManagedObject(name=f'SubAddition-{name}', type=f'Addition-{name}'))
194+
addition.add_child_addition(sub_addition)
195+
asset.add_child_addition(sub_addition)
196+
device.add_child_addition(sub_addition)
197+
198+
return obj.id
199+
200+
201+
def test_references(live_c8y: CumulocityApi, asset_hierarchy_root_id):
202+
"""Verify that parent references are handles as expected.
203+
204+
This test uses the "asset_hierarchy" fixture which defines a root
205+
with children of each kind.
206+
"""
207+
root_id = asset_hierarchy_root_id
208+
209+
# (1) ignore children and parents
210+
result = live_c8y.inventory.get(root_id, with_children=False)
211+
assert not result.child_assets
212+
assert not result.child_devices
213+
assert not result.child_additions
214+
assert not result.parent_assets
215+
assert not result.parent_devices
216+
assert not result.parent_additions
217+
218+
# (2) include children, with names
219+
result = live_c8y.inventory.get(root_id, with_children=True, skip_children_names=False)
220+
# -> the root object references one of each
221+
assert len(result.child_assets) == 1
222+
assert len(result.child_devices) == 1
223+
assert len(result.child_additions) == 1
224+
# -> including their names
225+
assert result.child_assets[0].name
226+
assert result.child_devices[0].name
227+
assert result.child_additions[0].name
228+
# -> but no parents
229+
assert not result.parent_assets
230+
assert not result.parent_devices
231+
assert not result.parent_additions
232+
233+
# (3) include children, no names
234+
result = live_c8y.inventory.get(root_id, with_children=True, skip_children_names=True)
235+
# -> the root object references one of each
236+
assert len(result.child_assets) == 1
237+
assert len(result.child_devices) == 1
238+
assert len(result.child_additions) == 1
239+
# -> including their names
240+
assert not result.child_assets[0].name
241+
assert not result.child_devices[0].name
242+
assert not result.child_additions[0].name
243+
244+
245+
@pytest.mark.parametrize('child_type', ['asset', 'device', 'addition'])
246+
def test_parent_references(live_c8y: CumulocityApi, asset_hierarchy_root_id, child_type):
247+
"""Verify that parent references are handles as expected.
248+
249+
This test uses the "asset_hierarchy" fixture which defines a root
250+
with children of each kind. Each kind has another "addition" child.
251+
"""
252+
root = live_c8y.inventory.get(asset_hierarchy_root_id, with_children=True)
253+
child = root.__dict__[f'child_{child_type}s'][0]
254+
255+
# read child with references
256+
result = live_c8y.inventory.get(child.id, with_children=True, with_parents=True)
257+
# parent (root) is linked by the child's type
258+
parents = result.__dict__[f'parent_{child_type}s']
259+
assert len(parents) == 1
260+
assert parents[0].id == root.id
261+
assert parents[0].name == root.name
262+
# each child as an 'addition' child
263+
assert len(result.child_additions) == 1
264+
265+
173266
def test_deletion(live_c8y: CumulocityApi, safe_create):
174267
"""Verify that deletion works as expected.
175268

tests/model/test_administration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
['username=U', 'groups=1,2,3', 'owner=O'],
1717
[]),
1818
({'only_devices': False, 'with_subusers_count': True},
19-
['onlyDevices=False', 'withSubusersCount=True'],
19+
['onlyDevices=false', 'withSubusersCount=true'],
2020
['_']),
2121
({'snake_case': 'SC', 'pascalCase': 'PC'},
2222
['snakeCase=SC', 'pascalCase=PC'],

tests/model/test_alarms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ def isolate_call_url(fun, **kwargs):
3737
@pytest.mark.parametrize('params, expected, not_expected', [
3838
({'expression': 'EX', 'type': 'T'}, ['?EX'], ['type']),
3939
({'type': 'T', 'source': 'S', 'fragment': 'F'}, ['type=T', 'source=S', 'fragmentType=F'], []),
40-
({'status': 'ST', 'severity': 'SE', 'resolved': False}, ['status=ST', 'severity=SE', 'resolved=False'], []),
40+
({'status': 'ST', 'severity': 'SE', 'resolved': False}, ['status=ST', 'severity=SE', 'resolved=false'], []),
4141
({'reverse': False, 'page_size': 8}, ['revert=false', 'pageSize=8'], ['reverse']),
4242
({'with_source_assets': False, 'with_source_devices': True, 'source': '123'},
43-
['withSourceAssets=False', 'withSourceDevices=True'],
43+
['withSourceAssets=false', 'withSourceDevices=true'],
4444
['_']),
4545
# data priorities
4646
({'date_from': '2020-12-31', 'date_to': '2021-12-31'},

tests/model/test_applications.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def isolate_call_url(fun, **kwargs):
3333
({'tenant': 'T', 'subscriber': 'S', 'provided_for': 'P'},
3434
['tenant=T', 'subscriber=S', 'providedFor=P'],
3535
['_']),
36-
({'has_versions': False}, ['hasVersions=False'], ['_']),
36+
({'has_versions': False}, ['hasVersions=false'], ['_']),
3737
({'snake_case': 'SC', 'pascalCase': 'PC'},
3838
['snakeCase=SC', 'pascalCase=PC'],
3939
['_']),

tests/model/test_events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ def isolate_call_url(fun, **kwargs):
3232
({'expression': 'EX', 'type': 'T'}, ['?EX'], ['type']),
3333
({'type': 'T', 'source': 'S', 'fragment': 'F'}, ['type=T', 'source=S', 'fragmentType=F'], []),
3434
({'fragment_type': 'T', 'fragment_value': 'V'}, ['fragmentType=T', 'fragmentValue=V'], ['_']),
35-
({'status': 'ST', 'severity': 'SE', 'resolved': False}, ['status=ST', 'severity=SE', 'resolved=False'], []),
35+
({'status': 'ST', 'severity': 'SE', 'resolved': False}, ['status=ST', 'severity=SE', 'resolved=false'], []),
3636
({'reverse': False}, ['revert=false'], ['reverse']),
3737
({'with_source_assets': False, 'with_source_devices': True, 'source': '123'},
38-
['withSourceAssets=False', 'withSourceDevices=True'],
38+
['withSourceAssets=false', 'withSourceDevices=true'],
3939
['_']),
4040
# data priorities
4141
({'date_from': '2020-12-31', 'date_to': '2021-12-31'},

0 commit comments

Comments
 (0)