Skip to content

Commit 8916add

Browse files
authored
Merge pull request #67 from Cumulocity-IoT/feature/cascade-deletion
Generic enhancements
2 parents e78599b + 92e58de commit 8916add

File tree

17 files changed

+328
-67
lines changed

17 files changed

+328
-67
lines changed

CHANGELOG.md

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

3+
* Added `reload` function to inventory object classes
4+
* Added `delete_tree` function to inventory object classes, implicitly using `cascade` or
5+
`forceCascade` parameters depending on the use case.
6+
* Added `__repr__` function to relevant object classes.
7+
* Removed redundant `util.py` file which impeded importing.
8+
39
## Version 3.1.1
410

511
* Fixing project dependencies for older Python versions.

c8y_api/model/_base.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# Copyright (c) 2020 Software AG,
2-
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3-
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4-
# Use, reproduction, transfer, publication or disclosure is prohibited except
5-
# as specifically provided for in your License Agreement with Software AG.
1+
# Copyright (c) 2025 Cumulocity GmbH
62

73
from __future__ import annotations
84

@@ -121,6 +117,20 @@ def _assert_id(self):
121117
if not self.id:
122118
raise ValueError("The object ID must be set to allow direct object access.")
123119

120+
def _repr(self, *names) -> str:
121+
return ''.join([
122+
type(self).__name__,
123+
'(',
124+
', '.join(filter(lambda x: x is not None,
125+
[
126+
f'{n}={self.__getattribute__(n)}' if self.__getattribute__(n) else None
127+
for n in ['id', *names]
128+
])),
129+
')'])
130+
131+
def __repr__(self) -> str:
132+
return self._repr()
133+
124134
@classmethod
125135
def _to_datetime(cls, timestring):
126136
if timestring:
@@ -338,12 +348,12 @@ def _update(self) -> Any[SimpleObject]:
338348
result.c8y = self.c8y
339349
return result
340350

341-
def _delete(self):
351+
def _delete(self, **params):
342352
self._assert_c8y()
343353
self._assert_id()
344-
self.c8y.delete(self._build_object_path())
354+
self.c8y.delete(self._build_object_path(), params=params)
345355

346-
def delete(self):
356+
def delete(self, **_):
347357
"""Delete the object within the database."""
348358
self._delete()
349359

c8y_api/model/_util.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
# Copyright (c) 2020 Software AG,
2-
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3-
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4-
# Use, reproduction, transfer, publication or disclosure is prohibited except
5-
# as specifically provided for in your License Agreement with Software AG.
1+
# Copyright (c) 2025 Cumulocity GmbH
62

73
import re
84
from datetime import datetime, timedelta, timezone
5+
from typing import Union
6+
97
from dateutil import parser
108
from re import sub
119

@@ -14,6 +12,14 @@ class _StringUtil(object):
1412

1513
TO_PASCAL_PATTERN = re.compile(r'_([a-z])')
1614

15+
@staticmethod
16+
def concat(*strings:Union[str, None]):
17+
return ''.join(x for x in strings if x)
18+
19+
@staticmethod
20+
def concat_with(sep: str, *strings:Union[str, None]):
21+
return sep.join(x for x in strings if x)
22+
1723
@staticmethod
1824
def to_pascal_case(name: str):
1925
"""Convert a given snake case (default Python style) name to pascal case (default for names in Cumulocity)"""

c8y_api/model/administration.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ def __init__(self, c8y: CumulocityRestApi = None, name: str = None, description:
147147
name = SimpleObject.UpdatableProperty('_u_name')
148148
description = SimpleObject.UpdatableProperty('_u_description')
149149

150+
def __repr__(self):
151+
return self._repr('name')
152+
150153
@classmethod
151154
def from_json(cls, json: dict) -> InventoryRole:
152155
# no doc change required
@@ -257,6 +260,9 @@ def __init__(self, c8y=None, name=None, description=None):
257260
name = SimpleObject.UpdatableProperty('_u_name')
258261
description = SimpleObject.UpdatableProperty('_u_description')
259262

263+
def __repr__(self):
264+
return self._repr('name')
265+
260266
@classmethod
261267
def from_json(cls, json: dict) -> GlobalRole:
262268
# no doc change
@@ -550,6 +556,9 @@ def __init__(self, c8y=None, username=None, email=None, enabled=True, display_na
550556
# self.effective_permission_ids = set()
551557
# self.custom_properties = WithUpdatableFragments()
552558

559+
def __repr__(self):
560+
return self._repr('username')
561+
553562
@classmethod
554563
def from_json(cls, json: dict) -> User:
555564
user = cls._from_json(json, User())

c8y_api/model/alarms.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ def first_occurrence_datetime(self) -> datetime:
128128
"""
129129
return super()._to_datetime(self.first_occurrence_time)
130130

131+
def __repr__(self):
132+
return self._repr('source', 'type', 'status', 'severity')
133+
131134
@classmethod
132135
def from_json(cls, json: dict) -> Alarm:
133136
"""Build a new Alarm instance from JSON.

c8y_api/model/binaries.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ def content_type(self) -> str:
3232
"""Content type set for this binary."""
3333
return self.contentType
3434

35+
def __repr__(self):
36+
return self._repr('name', 'type')
37+
3538
@classmethod
3639
def from_json(cls, json: dict) -> Binary:
3740
""" Build a new Binary instance from JSON.

c8y_api/model/events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ def updated_datetime(self) -> datetime:
9494
def _build_attachment_path(self) -> str:
9595
return super()._build_object_path() + '/binaries'
9696

97+
def __repr__(self):
98+
return self._repr('source', 'type')
99+
97100
@classmethod
98101
def from_json(cls, json: dict) -> Event:
99102
# (no doc update required)

c8y_api/model/identity.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ def __init__(self, c8y: CumulocityRestApi = None, external_id: str = None, exter
4949
self.external_type = external_type
5050
self.managed_object_id = managed_object_id
5151

52+
def __repr__(self):
53+
return self._repr('external_type', 'external_id', 'managed_object_id')
54+
5255
@classmethod
5356
def from_json(cls, json: dict) -> ExternalId:
5457
""" Build a new ExternalId instance from JSON.

c8y_api/model/inventory.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
# Copyright (c) 2020 Software AG,
2-
# Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA,
3-
# and/or its subsidiaries and/or its affiliates and/or their licensors.
4-
# Use, reproduction, transfer, publication or disclosure is prohibited except
5-
# as specifically provided for in your License Agreement with Software AG.
1+
# Copyright (c) 2025 Cumulocity GmbH
62
# pylint: disable=too-many-lines
73

84
from __future__ import annotations
@@ -966,7 +962,7 @@ def delete_trees(self, *groups: DeviceGroup | str):
966962
Args:
967963
*groups (str|DeviceGroup): Collection of objects (or ID).
968964
"""
969-
self._delete(False, *groups)
965+
self._delete(True, *groups)
970966

971967
def _delete(self, cascade: bool, *objects: DeviceGroup | str):
972968
try:

c8y_api/model/managedobjects.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Copyright (c) 2025 Cumulocity GmbH
2+
13
from __future__ import annotations
24

35
from datetime import datetime
@@ -7,7 +9,7 @@
79
from c8y_api.model.administration import User, Users
810
from c8y_api.model._base import _DictWrapper, SimpleObject, ComplexObject
911
from c8y_api.model._parser import ComplexObjectParser
10-
from c8y_api.model._util import _DateUtil
12+
from c8y_api.model._util import _DateUtil, _StringUtil
1113

1214

1315
class NamedObject(object):
@@ -316,6 +318,9 @@ def update_datetime(self):
316318
"""
317319
return super()._to_datetime(self.update_time)
318320

321+
def __repr__(self):
322+
return self._repr('name', 'type')
323+
319324
@classmethod
320325
def _from_json(cls, json: dict, obj: Any) -> Any:
321326
# pylint: disable=arguments-differ
@@ -365,6 +370,22 @@ def to_json(self, only_updated=False) -> dict:
365370
def _parse_references(cls, base_json):
366371
return [NamedObject.from_json(j['managedObject']) for j in base_json['references']]
367372

373+
def _reload(self, new_object):
374+
self._assert_c8y()
375+
self._assert_id()
376+
result = self._from_json(self.c8y.get(self._build_object_path()), new_object)
377+
result.c8y = self.c8y
378+
return result
379+
380+
def reload(self) -> ManagedObject:
381+
"""Reload this object's data from database.
382+
383+
Returns:
384+
New ManagedObject instance built from latest data.
385+
"""
386+
return self._reload(ManagedObject())
387+
388+
368389
def create(self) -> ManagedObject:
369390
""" Create a new representation of this object within the database.
370391
@@ -415,12 +436,26 @@ def apply_to(self, other_id: str | int) -> ManagedObject:
415436
def delete(self):
416437
""" Delete this object within the database.
417438
418-
The database ID must be defined for this to function.
439+
Note: child additions, assets (and devices) are not implicitly
440+
deleted. The database ID must be defined for this to function.
419441
420442
See also function Inventory.delete to delete multiple objects.
421443
"""
422444
self._delete()
423445

446+
def delete_tree(self):
447+
"""Delete this managed object within the database including child.
448+
additions, devices and assets.
449+
This is equivalent to using the `forceCascade` parameter of the
450+
Cumulocity REST API.
451+
452+
The database ID must be defined for this to function.
453+
454+
See also function DeviceInventory.delete_trees to delete multiple objects.
455+
"""
456+
self._delete(forceCascade='true')
457+
458+
424459
def add_child_asset(self, child: ManagedObject | str | int):
425460
""" Link a child asset to this managed object.
426461
@@ -601,6 +636,49 @@ def get_user(self) -> User:
601636
"""
602637
return Users(self.c8y).get(self.get_username())
603638

639+
def reload(self) -> Device:
640+
"""Reload this object's data from database.
641+
642+
Returns:
643+
New Device instance built from latest data.
644+
"""
645+
return self._reload(Device())
646+
647+
def delete(self, with_device_user=False):
648+
"""Delete this device object within the database.
649+
650+
Note: child additions, assets (and devices) are not implicitly
651+
deleted. The database ID must be defined for this to function.
652+
653+
Args:
654+
with_device_user (bool): Whether the device user is deleted
655+
as well.
656+
657+
See also function DeviceInventory.delete to delete multiple objects.
658+
"""
659+
if with_device_user:
660+
self._delete(withDeviceUser='true')
661+
else:
662+
self._delete()
663+
664+
def delete_tree(self, with_device_user=False):
665+
"""Delete this device object within the database including child.
666+
additions, devices and assets.
667+
668+
The database ID must be defined for this to function.
669+
670+
Args:
671+
with_device_user (bool): Whether the device user is deleted
672+
as well.
673+
674+
See also function DeviceInventory.delete to delete multiple objects.
675+
"""
676+
if with_device_user:
677+
self._delete(cascade='true', withDeviceUser='true')
678+
else:
679+
self._delete(cascade='true')
680+
681+
604682

605683
class DeviceGroup(ManagedObject):
606684
""" Represent a device group within Cumulocity.
@@ -663,6 +741,14 @@ def from_json(cls, json: dict) -> DeviceGroup:
663741
"""
664742
return super()._from_json(json, DeviceGroup())
665743

744+
def reload(self) -> DeviceGroup:
745+
"""Reload this object's data from database.
746+
747+
Returns:
748+
New DeviceGroup instance built from latest data.
749+
"""
750+
return self._reload(DeviceGroup())
751+
666752
def create_child(self, name: str, owner: str = None, **kwargs) -> DeviceGroup:
667753
""" Create and assign a child group.
668754
@@ -715,19 +801,15 @@ def delete(self):
715801
equivalent to using the `cascade=false` parameter in the
716802
Cumulocity REST API.
717803
"""
718-
self._assert_c8y()
719-
self._assert_id()
720-
self.c8y.delete(self._build_object_path() + '?cascade=false')
804+
self._delete(cascade='false')
721805

722806
def delete_tree(self):
723807
"""Delete this device group and its children.
724808
725809
This is equivalent to using the `cascade=true` parameter in the
726810
Cumulocity REST API.
727811
"""
728-
self._assert_c8y()
729-
self._assert_id()
730-
self.c8y.delete(self._build_object_path() + '?cascade=true')
812+
self._delete(cascade='true')
731813

732814
def assign_child_group(self, child: DeviceGroup | str | int):
733815
"""Link a child group to this device group.

0 commit comments

Comments
 (0)