Skip to content

Commit 75db840

Browse files
committed
fix: prevent entity list to call back after last iteration
1 parent ecff8a8 commit 75db840

1 file changed

Lines changed: 169 additions & 162 deletions

File tree

Lines changed: 169 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,169 @@
1-
# Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
2-
# Karlsruhe, Germany.
3-
#
4-
# This program is free software: you can redistribute it and/or modify
5-
# it under the terms of the GNU Lesser General Public License as published by
6-
# the Free Software Foundation, either version 3 of the License, or
7-
# (at your option) any later version.
8-
#
9-
# This program is distributed in the hope that it will be useful,
10-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12-
# GNU Lesser General Public License for more details.
13-
#
14-
# You should have received a copy of the GNU Lesser General Public License
15-
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16-
17-
import logging
18-
import requests
19-
import frost_sta_client
20-
21-
22-
class EntityList:
23-
def __init__(self, entity_class, entities=None):
24-
if entities is None:
25-
entities = []
26-
self.entities = entities
27-
self.entity_class = entity_class
28-
self.next_link = None
29-
self.service = None
30-
self.iterable_entities = None
31-
self.count = None
32-
self.callback = None
33-
self.step_size = None
34-
35-
def __new__(cls, *args, **kwargs):
36-
new_entity_list = super().__new__(cls)
37-
attributes = {'_entities': None, '_entity_class': '', '_next_link': '', '_service': {}, '_count': '',
38-
'_iterable_entities': None, '_callback': None,
39-
'_step_size': None}
40-
for key, value in attributes.items():
41-
new_entity_list.__dict__[key] = value
42-
return new_entity_list
43-
44-
def __iter__(self):
45-
self.iterable_entities = iter(enumerate(self.entities))
46-
return self
47-
48-
def __next__(self):
49-
idx, next_entity = next(self.iterable_entities, (len(self.entities), None))
50-
if self.step_size is not None and idx is not None and idx % self.step_size == 0:
51-
self.callback(idx)
52-
if next_entity is not None:
53-
return next_entity
54-
if self.next_link is not None:
55-
try:
56-
response = self.service.execute('get', self.next_link)
57-
except requests.exceptions.HTTPError as e:
58-
error_json = e.response.json()
59-
error_message = error_json['message']
60-
logging.error("Query failed with status-code {}, {}".format(e.response.status_code, error_message))
61-
raise e
62-
logging.debug('Received response: {} from {}'.format(response.status_code, self.next_link))
63-
try:
64-
json_response = response.json()
65-
except ValueError:
66-
raise ValueError('Cannot find json in http response')
67-
68-
result_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class)
69-
self.entities += result_list.entities
70-
self.set_service(self.service)
71-
self.next_link = json_response.get("@iot.nextLink", None)
72-
self.iterable_entities = iter(enumerate(self.entities[-len(result_list.entities):], start=idx))
73-
return next(self.iterable_entities)[1]
74-
raise StopIteration
75-
76-
def get(self, index):
77-
if not isinstance(index, int):
78-
raise IndexError('index must be an integer')
79-
if index >= len(self.entities):
80-
raise IndexError('index exceeds total number of entities')
81-
if index < 0:
82-
raise IndexError('negative indices cannot be accessed')
83-
return self.entities[index]
84-
85-
@property
86-
def entity_class(self):
87-
return self._entity_class
88-
89-
@entity_class.setter
90-
def entity_class(self, value):
91-
if isinstance(value, str):
92-
self._entity_class = value
93-
return
94-
raise ValueError('entity_class should be of type str')
95-
96-
@property
97-
def entities(self):
98-
return self._entities
99-
100-
@entities.setter
101-
def entities(self, values):
102-
if isinstance(values, list) and all(isinstance(v, frost_sta_client.model.entity.Entity) for v in values):
103-
self._entities = values
104-
return
105-
raise ValueError('entities should be a list of entities')
106-
107-
@property
108-
def callback(self):
109-
return self._callback
110-
111-
@callback.setter
112-
def callback(self, callback):
113-
if callable(callback) or callback is None:
114-
self._callback = callback
115-
116-
@property
117-
def step_size(self):
118-
return self._step_size
119-
120-
@step_size.setter
121-
def step_size(self, value):
122-
if isinstance(value, int) or value is None:
123-
self._step_size = value
124-
return
125-
raise ValueError('step_size should be of type int')
126-
127-
@property
128-
def next_link(self):
129-
return self._next_link
130-
131-
@next_link.setter
132-
def next_link(self, value):
133-
if value is None or isinstance(value, str):
134-
self._next_link = value
135-
return
136-
raise ValueError('next_link should be of type string')
137-
138-
@property
139-
def service(self):
140-
return self._service
141-
142-
@service.setter
143-
def service(self, value):
144-
if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService):
145-
self._service = value
146-
return
147-
raise ValueError('service should be of type SensorThingsService')
148-
149-
def set_service(self, service):
150-
self.service = service
151-
for entity in self.entities:
152-
entity.set_service(service)
153-
154-
def __getstate__(self):
155-
data = []
156-
for entity in self.entities:
157-
data.append(entity.__getstate__())
158-
return data
159-
160-
def __setstate__(self, state):
161-
self._next_link = state.get(self.entities + '@nextLink')
162-
pass
1+
# Copyright (C) 2021 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
2+
# Karlsruhe, Germany.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU Lesser General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import logging
18+
import requests
19+
import frost_sta_client
20+
21+
22+
class EntityList:
23+
def __init__(self, entity_class, entities=None):
24+
if entities is None:
25+
entities = []
26+
self.entities = entities
27+
self.entity_class = entity_class
28+
self.next_link = None
29+
self.service = None
30+
self.iterable_entities = None
31+
self.count = None
32+
self.callback = None
33+
self.step_size = None
34+
35+
def __new__(cls, *args, **kwargs):
36+
new_entity_list = super().__new__(cls)
37+
attributes = {'_entities': None, '_entity_class': '', '_next_link': '', '_service': {}, '_count': '',
38+
'_iterable_entities': None, '_callback': None,
39+
'_step_size': None}
40+
for key, value in attributes.items():
41+
new_entity_list.__dict__[key] = value
42+
return new_entity_list
43+
44+
def __iter__(self):
45+
self.iterable_entities = iter(enumerate(self.entities))
46+
return self
47+
48+
def __next__(self):
49+
idx, next_entity = next(self.iterable_entities, (None, None))
50+
# Only trigger callback when returning a real entity, not on sentinel indices
51+
if next_entity is None:
52+
# If current page is exhausted, try to load the next page
53+
if self.next_link is None:
54+
raise StopIteration
55+
try:
56+
response = self.service.execute('get', self.next_link)
57+
except requests.exceptions.HTTPError as e:
58+
error_json = e.response.json()
59+
error_message = error_json['message']
60+
logging.error("Query failed with status-code {}, {}".format(e.response.status_code, error_message))
61+
raise e
62+
logging.debug('Received response: {} from {}'.format(response.status_code, self.next_link))
63+
try:
64+
json_response = response.json()
65+
except ValueError:
66+
raise ValueError('Cannot find json in http response')
67+
68+
result_list = frost_sta_client.utils.transform_json_to_entity_list(json_response, self.entity_class)
69+
# Append new entities and reset iterator to iterate over the newly fetched page
70+
start_index = len(self.entities)
71+
self.entities += result_list.entities
72+
self.set_service(self.service)
73+
self.next_link = json_response.get("@iot.nextLink", None)
74+
self.iterable_entities = iter(enumerate(self.entities[start_index:], start=start_index))
75+
idx, next_entity = next(self.iterable_entities, (None, None))
76+
if next_entity is None:
77+
raise StopIteration
78+
if self.step_size is not None and self.callback is not None and idx % self.step_size == 0:
79+
self.callback(idx)
80+
return next_entity
81+
raise StopIteration
82+
83+
def get(self, index):
84+
if not isinstance(index, int):
85+
raise IndexError('index must be an integer')
86+
if index >= len(self.entities):
87+
raise IndexError('index exceeds total number of entities')
88+
if index < 0:
89+
raise IndexError('negative indices cannot be accessed')
90+
return self.entities[index]
91+
92+
@property
93+
def entity_class(self):
94+
return self._entity_class
95+
96+
@entity_class.setter
97+
def entity_class(self, value):
98+
if isinstance(value, str):
99+
self._entity_class = value
100+
return
101+
raise ValueError('entity_class should be of type str')
102+
103+
@property
104+
def entities(self):
105+
return self._entities
106+
107+
@entities.setter
108+
def entities(self, values):
109+
if isinstance(values, list) and all(isinstance(v, frost_sta_client.model.entity.Entity) for v in values):
110+
self._entities = values
111+
return
112+
raise ValueError('entities should be a list of entities')
113+
114+
@property
115+
def callback(self):
116+
return self._callback
117+
118+
@callback.setter
119+
def callback(self, callback):
120+
if callable(callback) or callback is None:
121+
self._callback = callback
122+
123+
@property
124+
def step_size(self):
125+
return self._step_size
126+
127+
@step_size.setter
128+
def step_size(self, value):
129+
if isinstance(value, int) or value is None:
130+
self._step_size = value
131+
return
132+
raise ValueError('step_size should be of type int')
133+
134+
@property
135+
def next_link(self):
136+
return self._next_link
137+
138+
@next_link.setter
139+
def next_link(self, value):
140+
if value is None or isinstance(value, str):
141+
self._next_link = value
142+
return
143+
raise ValueError('next_link should be of type string')
144+
145+
@property
146+
def service(self):
147+
return self._service
148+
149+
@service.setter
150+
def service(self, value):
151+
if value is None or isinstance(value, frost_sta_client.service.sensorthingsservice.SensorThingsService):
152+
self._service = value
153+
return
154+
raise ValueError('service should be of type SensorThingsService')
155+
156+
def set_service(self, service):
157+
self.service = service
158+
for entity in self.entities:
159+
entity.set_service(service)
160+
161+
def __getstate__(self):
162+
data = []
163+
for entity in self.entities:
164+
data.append(entity.__getstate__())
165+
return data
166+
167+
def __setstate__(self, state):
168+
self._next_link = state.get(self.entities + '@nextLink')
169+
pass

0 commit comments

Comments
 (0)