Skip to content

Commit 6134a5f

Browse files
authored
Merge pull request #79 from Cumulocity-IoT/feature/inventory_get_by
2 parents cb1de86 + 7a88801 commit 6134a5f

14 files changed

+326
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Changelog
22

3+
* Added `get_by` function to inventory API to return a single object by query.
34
* Added `CumulocityApp` class to module `c8y_tk.app` which allows working with Cumulocity interactively, e.g. in a
45
Jupyther notebook. It will deal with environment variables just like the other connection helpers but will also
56
ask interactively for missing info, e.g. a second factor with 2FA. It also integrates well with the

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: 120 additions & 6 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

@@ -98,6 +125,66 @@ def get_all(
98125
as_values=as_values,
99126
**kwargs))
100127

128+
def get_by(
129+
self,
130+
expression: str = None,
131+
query: str = None,
132+
ids: list[str | int] = None,
133+
order_by: str = None,
134+
type: str = None,
135+
parent: str = None,
136+
fragment: str = None,
137+
fragments: list[str] = None,
138+
name: str = None,
139+
owner: str = None,
140+
text: str = None,
141+
only_roots: str = None,
142+
with_children: bool = None,
143+
with_children_count: bool = None,
144+
skip_children_names: bool = None,
145+
with_groups: bool = None,
146+
with_parents: bool = None,
147+
with_latest_values: bool = None,
148+
as_values: str | tuple | list[str | tuple] = None,
149+
**kwargs) -> ManagedObject:
150+
""" Query the database for a specific managed object.
151+
152+
This function is a special version of the `select` function assuming a single
153+
result being returned by the query.
154+
155+
Returns:
156+
A ManagedObject instance
157+
158+
Raises:
159+
ValueError: if the query did not return any or more than one result.
160+
"""
161+
result = list(self.select(
162+
expression=expression,
163+
query=query,
164+
ids=ids,
165+
order_by=order_by,
166+
type=type,
167+
parent=parent,
168+
fragment=fragment,
169+
fragments=fragments,
170+
name=name,
171+
owner=owner,
172+
text=text,
173+
only_roots=only_roots,
174+
with_children=with_children,
175+
with_children_count=with_children_count,
176+
skip_children_names=skip_children_names,
177+
with_groups=with_groups,
178+
with_parents=with_parents,
179+
with_latest_values=with_latest_values,
180+
page_size=2,
181+
as_values=as_values,
182+
**kwargs))
183+
if len(result) == 1:
184+
return result[0]
185+
raise ValueError("No matching object found." if not result
186+
else "Ambiguous query; multiple matching objects found.")
187+
101188
def get_count(
102189
self,
103190
expression: str = None,
@@ -455,19 +542,46 @@ def accept(self, id: str): # noqa (id)
455542
"""
456543
self.c8y.put('/devicecontrol/newDeviceRequests/' + str(id), {'status': 'ACCEPTED'})
457544

458-
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:
459554
""" Retrieve a specific device object.
460555
461556
Args:
462-
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.
463569
464570
Returns:
465571
A Device instance
466572
467573
Raises:
468574
KeyError: if the ID is not defined within the database
469575
"""
470-
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+
)
471585
device.c8y = self.c8y
472586
return device
473587

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

0 commit comments

Comments
 (0)