Skip to content

Commit

Permalink
Added get_count functions to inventory; Added possibility to pull a s…
Browse files Browse the repository at this point in the history
…pecific page number.
  • Loading branch information
chsou committed Dec 7, 2023
1 parent 7920ed6 commit 615bf5d
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 53 deletions.
8 changes: 6 additions & 2 deletions c8y_api/model/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,15 +508,19 @@ def _prepare_query_params(type=None, name=None, fragment=None, source=None, # n

def _build_base_query(self, **kwargs):
params = CumulocityResource._prepare_query_params(**kwargs)
return self.resource + '?' + urlencode(params) + '&currentPage='
return self.resource + '?' + urlencode(params)

def _get_object(self, object_id):
return self.c8y.get(self.build_object_path(object_id))

def _get_page(self, base_query: str, page_number: int):
result_json = self.c8y.get(base_query + str(page_number))
result_json = self.c8y.get(base_query + '&currentPage=' + str(page_number))
return result_json[self.object_name]

def _get_count(self, base_query: str) -> int:
result_json = self.c8y.get(base_query + '&withTotalPages=true')
return result_json['statistics']['totalPages']

def _iterate(self, base_query: str, page_number: int | None, limit: int, parse_func):
# if no specific page is defined we just start at 1
current_page = page_number if page_number else 1
Expand Down
4 changes: 1 addition & 3 deletions c8y_api/model/alarms.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,4 @@ def delete_by(self, type: str = None, source: str = None, fragment: str = None,
base_query = self._build_base_query(type=type, source=source, fragment=fragment,
status=status, severity=severity, resolved=resolved,
before=before, after=after, min_age=min_age, max_age=max_age)
# remove &page_number= from the end
query = base_query[:base_query.rindex('&')]
self.c8y.delete(query)
self.c8y.delete(base_query)
134 changes: 93 additions & 41 deletions c8y_api/model/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@

from __future__ import annotations

from datetime import datetime
from typing import Any, Generator, List

from c8y_api.model._base import CumulocityResource
from c8y_api.model._util import _DateUtil, _QueryUtil
from c8y_api.model._util import _QueryUtil
from c8y_api.model.managedobjects import ManagedObjectUtil, ManagedObject, Device, Availability, DeviceGroup


Expand Down Expand Up @@ -56,6 +55,19 @@ def get_all(self, type: str = None, fragment: str = None, name: str = None, owne
"""
return list(self.select(type=type, fragment=fragment, name=name, owner=owner, limit=limit, page_size=page_size))

def get_count(self, type: str = None, fragment: str = None, name: str = None,
owner: str = None, query: str = None) -> int: # noqa (type)
"""Calculate the number of potential results of a database query.
This function uses the same parameters as the `select` function.
Returns:
Number of potential results
"""
base_query = self._prepare_query(type=type, fragment=fragment, name=name,
owner=owner, query=query, page_size=1)
return self._get_count(base_query)

def select(self, type: str = None, fragment: str = None, name: str = None, owner: str = None, # noqa (type)
query: str = None, limit: int = None,
page_size: int = 1000, page_number: int = None) -> Generator[ManagedObject]:
Expand Down Expand Up @@ -91,10 +103,10 @@ def select(self, type: str = None, fragment: str = None, name: str = None, owner
return self._select(ManagedObject.from_json, type=type, fragment=fragment, name=name, owner=owner,
query=None, limit=limit, page_size=page_size, page_number=page_number)

def _select(self, jsonyfy_func, type: str = None, fragment: str = None, name: str = None, # noqa
owner: str = None, query: str = None, limit: int = None,
page_size: int = 1000, page_number: int = None) -> Generator[Any]:

def _prepare_query(self, type: str = None, fragment: str = None, name: str = None, # noqa
owner: str = None, query: str = None, page_size: int = 1000) -> str:
"""The inventory API features a query API that needs some additional
preparations before we can actually invoke the queries."""
query_filters = []

# if there is no custom query, we check whether standard filters need to
Expand All @@ -117,11 +129,16 @@ def _select(self, jsonyfy_func, type: str = None, fragment: str = None, name: st
query = '$filter=(' + ' and '.join(query_filters) + ')'

if query:
base_query = self._build_base_query(query=query, page_size=page_size)
else:
base_query = self._build_base_query(type=type, fragment=fragment, owner=owner, page_size=page_size)
return self._build_base_query(query=query, page_size=page_size)
return self._build_base_query(type=type, fragment=fragment, owner=owner, page_size=page_size)

def _select(self, jsonyfy_func, type: str = None, fragment: str = None, name: str = None, # noqa
owner: str = None, query: str = None, limit: int = None,
page_size: int = 1000, page_number: int = None) -> Generator[Any]:
base_query = self._prepare_query(type=type, fragment=fragment, name=name,
owner=owner, query=query, page_size=page_size)
return super()._iterate(base_query, page_number, limit, jsonyfy_func)

return super()._iterate(base_query, page_number, limit, jsonyfy_func)

def create(self, *objects: ManagedObject):
"""Create managed objects within the database.
Expand Down Expand Up @@ -241,7 +258,7 @@ def get(self, id: str) -> Device: # noqa (id)
def select(self, type: str = None, name: str = None, owner: str = None, # noqa (type, args)
query: str = None, limit: int = None,
page_size: int = 100, page_number: int = None) -> Generator[Device]:
# pylint: disable=arguments-differ
# pylint: disable=arguments-differ, arguments-renamed
""" Query the database for devices and iterate over the results.
This function is implemented in a lazy fashion - results will only be
Expand All @@ -251,6 +268,9 @@ def select(self, type: str = None, name: str = None, owner: str = None, # noqa
to objects which meet the filters specification. Filters can be
combined (within reason).
Note: this variant doesn't allow filtering by fragment because the
`c8y_IsDevice` fragment is automatically filtered.
Args:
type (str): Device type
name (str): Name of the device
Expand All @@ -273,8 +293,8 @@ def select(self, type: str = None, name: str = None, owner: str = None, # noqa
query=query, limit=limit, page_size=page_size, page_number=page_number)

def get_all(self, type: str = None, name: str = None, owner: str = None, # noqa (type, parameters)
page_size: int = 100, page_number: int = None) -> List[Device]:
# pylint: disable=arguments-differ
limit: int = None, page_size: int = 100, page_number: int = None) -> List[Device]:
# pylint: disable=arguments-differ, arguments-renamed
""" Query the database for devices and return the results as list.
This function is a greedy version of the `select` function. All
Expand All @@ -283,7 +303,19 @@ def get_all(self, type: str = None, name: str = None, owner: str = None, # noq
Returns:
List of Device objects
"""
return list(self.select(type=type, name=name, owner=owner, page_size=page_size, page_number=page_number))
return list(self.select(type=type, name=name, owner=owner, limit=limit,
page_size=page_size, page_number=page_number))

def get_count(self, type: str = None, name: str = None, owner: str = None) -> int: # noqa
# pylint: disable=arguments-differ, arguments-renamed
"""Calculate the number of potential results of a database query.
This function uses the same parameters as the `select` function.
Returns:
Number of potential results
"""
return self._get_count(super()._prepare_query(type=type, name=name, owner=owner, page_size=1))

def delete(self, *devices: Device):
""" Delete one or more devices and the corresponding within the database.
Expand Down Expand Up @@ -330,6 +362,39 @@ def get(self, group_id: str):
group.c8y = self.c8y
return group

def _prepare_query(self, type: str, parent: str | int = None, fragment: str = None, # noqa
name: str = None, owner: str = None, query: str = None, page_size: int = 100) -> str:
# pylint: disable=arguments-differ, arguments-renamed
query_filters = []

# if there is no custom query, we check whether standard filters need to
# be translated into a query
if not query:

# Both name and parent filters can only be expressed as a query,
# which then triggers "query mode"
if name:
query_filters.append(f"name eq '{_QueryUtil.encode_odata_query_value(name)}'")
if parent:
query_filters.append(f"bygroupid({parent})")
type = DeviceGroup.CHILD_TYPE # noqa

# if any query was defined, all filters must be put into the query
if query_filters:
if type:
query_filters.append(f"type eq '{type}'")
if owner:
query_filters.append(f"owner eq '{owner}'")
if fragment:
query_filters.append(f"has({fragment}")
query = '$filter=' + ' and '.join(query_filters)

if query:
return self._build_base_query(query=query, page_size=page_size)

return self._build_base_query(type=type, fragment=fragment, owner=owner, page_size=page_size)


def select(self, type: str = DeviceGroup.ROOT_TYPE, parent: str | int = None, fragment: str = None, # noqa
name: str = None, owner: str = None, query: str = None,
page_size: int = 100, page_number: int = None) -> Generator[DeviceGroup]:
Expand Down Expand Up @@ -369,36 +434,23 @@ def select(self, type: str = DeviceGroup.ROOT_TYPE, parent: str | int = None, fr
Returns:
Generator of DeviceGroup instances
"""
query_filters = []

# if there is no custom query, we check whether standard filters need to
# be translated into a query
if not query:

# Both name and parent filters can only be expressed as a query,
# which then triggers "query mode"
if name:
query_filters.append(f"name eq '{_QueryUtil.encode_odata_query_value(name)}'")
if parent:
query_filters.append(f"bygroupid({parent})")
type = DeviceGroup.CHILD_TYPE # noqa
base_query = self._prepare_query(type=type, parent=parent, fragment=fragment,
name=name, owner=owner, query=query, page_size=page_size)
return super()._iterate(base_query, page_number, limit=9999, parse_func=DeviceGroup.from_json)

# if any query was defined, all filters must be put into the query
if query_filters:
if type:
query_filters.append(f"type eq '{type}'")
if owner:
query_filters.append(f"owner eq '{owner}'")
if fragment:
query_filters.append(f"has({fragment}")
query = '$filter=' + ' and '.join(query_filters)
def get_count(self, type: str = DeviceGroup.ROOT_TYPE, parent: str | int = None, fragment: str = None,
name: str = None, owner: str = None, query: str = None) -> int:
# pylint: disable=arguments-differ, arguments-renamed
"""Calculate the number of potential results of a database query.
if query:
base_query = self._build_base_query(query=query, page_size=page_size)
else:
base_query = self._build_base_query(type=type, fragment=fragment, owner=owner, page_size=page_size)
This function uses the same parameters as the `select` function.
return super()._iterate(base_query, page_number, limit=9999, parse_func=DeviceGroup.from_json)
Returns:
Number of potential results
"""
base_query = self._prepare_query(type=type, parent=parent, fragment=fragment,
name=name, owner=owner, query=query, page_size=1)
return self._get_count(base_query)

def get_all(self, type: str = DeviceGroup.ROOT_TYPE, parent: str | int = None, fragment: str = None, # noqa
name: str = None, owner: str = None, page_size: int = 100, page_number: int = None): # noqa
Expand Down
4 changes: 1 addition & 3 deletions c8y_api/model/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,7 @@ def delete_by(self, agent_id: str = None, device_id: str = None, status: str = N
base_query = self._build_base_query(agent_id=agent_id, device_id=device_id, status=status,
bulk_id=bulk_id, fragment=fragment,
before=before, after=after, min_age=min_age, max_age=max_age)
# remove &page_number= from the end
query = base_query[:base_query.rindex('&')]
self.c8y.delete(query)
self.c8y.delete(base_query)


class BulkOperation(ComplexObject):
Expand Down
5 changes: 4 additions & 1 deletion integration_tests/test_devicegroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_CRUD(live_c8y: CumulocityApi, safe_executor):
with pytest.raises(KeyError):
live_c8y.group_inventory.get(child2.id)

# x) delete root and cascase
# x) delete root and cascade
root.delete_tree()
# -> root and remaining child are gone
with pytest.raises(KeyError):
Expand Down Expand Up @@ -138,17 +138,20 @@ def test_select(live_c8y: CumulocityApi, safe_executor):
ids = [x.id for x in live_c8y.group_inventory.select(name=f'Root-{name}')]
# -> only the root group is returned
assert ids == [root.id]
assert live_c8y.group_inventory.get_count(name=f'Root-{name}') == 1

# 2) select child folders via owner (no query)
found_ids = [x.id for x in live_c8y.group_inventory.select(type='c8y_DeviceSubGroup', owner=live_c8y.username)]
# -> only the child group can be found
assert child1.id in found_ids
assert root.id not in found_ids
assert live_c8y.group_inventory.get_count(type='c8y_DeviceSubGroup', owner=live_c8y.username) == len(found_ids)

# 3) select child by parent and owner (implicit query)
ids = [x.id for x in live_c8y.group_inventory.select(parent=root.id, owner=live_c8y.username)]
# -> only the child is returned
assert ids == [child1.id]
assert live_c8y.group_inventory.get_count(parent=root.id, owner=live_c8y.username) == 1

root.delete_tree()

Expand Down
1 change: 1 addition & 0 deletions integration_tests/test_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def test_get_by_something(live_c8y: CumulocityApi, similar_objects: List[Managed
kwargs = {key: value_fun(similar_objects[0])}
selected_mos = live_c8y.inventory.get_all(**kwargs)
assert get_ids(similar_objects) == get_ids(selected_mos)
assert live_c8y.inventory.get_count(**kwargs) == len(similar_objects)


def test_get_availability(live_c8y: CumulocityApi, sample_device: Device):
Expand Down
3 changes: 0 additions & 3 deletions tests/model/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,3 @@ def test_build_base_query():
# -> all expected params are there
for key, value in expected_params.items():
assert f'{key}={value}' in base_query

# -> query string ends with currentPage param
assert base_query.endswith('currentPage=')

0 comments on commit 615bf5d

Please sign in to comment.