Skip to content

Commit 6cc1865

Browse files
authored
Merge pull request #120 from ServiceNow/scratch/address-link-failure
fix: tests and update
2 parents d6a4f7d + 8fdeb2c commit 6cc1865

File tree

7 files changed

+396
-263
lines changed

7 files changed

+396
-263
lines changed

docs/user/advanced.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,7 @@ The following can improve performance:
132132
* Increase (or decrease) the default :ref:`batch_size` for GlideRecord
133133
* According to `KB0534905 <https://support.servicenow.com/kb_view.do?sysparm_article=KB0534905>`_ try disabling display values if they are not required via `gr.display_value = False`
134134
* Try setting a query :ref:`limit` if you do not need all results
135+
136+
2. Why am I consuming so much memory?
137+
138+
By default, GlideRecord can be 'rewound' in that it stores all previously fetched records in memory. This can be disabled by setting :ref:`rewind` to False on GlideRecord

poetry.lock

Lines changed: 266 additions & 233 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pysnc/client.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,14 @@ def _transform_response(self, req: requests.PreparedRequest, serviced_request) -
365365

366366
return response
367367

368-
def execute(self):
368+
def execute(self, attempt=0):
369+
if attempt > 2:
370+
# just give up and tell em we tried
371+
for h in self.__hooks:
372+
self.__hooks[h](None)
373+
self.__hooks = {}
374+
self.__requests = []
375+
self.__stored_requests = {}
369376
bid = self._next_id()
370377
body = {
371378
'batch_request_id': bid,
@@ -374,20 +381,18 @@ def execute(self):
374381
r = self.session.post(self._batch_target(), json=body)
375382
self._validate_response(r)
376383
data = r.json()
377-
try:
378-
assert str(bid) == data['batch_request_id'], f"How did we get a response id different from {bid}"
379-
380-
for response in data['serviced_requests'] + data['unserviced_requests']:
381-
response_id = response['id']
382-
assert response_id in self.__hooks, f"Somehow has no hook for {response_id}"
383-
assert response_id in self.__stored_requests, f"Somehow we did not store request for {response_id}"
384-
self.__hooks[response['id']](self._transform_response(self.__stored_requests[response_id], response))
385-
386-
return bid
387-
finally:
388-
self.__stored_requests = {}
389-
self.__requests = []
390-
self.__hooks = {}
384+
assert str(bid) == data['batch_request_id'], f"How did we get a response id different from {bid}"
385+
386+
for response in data['serviced_requests']:
387+
response_id = response['id']
388+
assert response_id in self.__hooks, f"Somehow has no hook for {response_id}"
389+
assert response_id in self.__stored_requests, f"Somehow we did not store request for {response_id}"
390+
self.__hooks[response['id']](self._transform_response(self.__stored_requests.pop(response_id), response))
391+
del self.__hooks[response_id]
392+
self.__requests = list(filter(lambda x: x['id'] != response_id, self.__requests))
393+
394+
if len(data['unserviced_requests']) > 0:
395+
self.execute(attempt=attempt+1)
391396

392397
def get(self, record: GlideRecord, sys_id: str, hook: Callable) -> None:
393398
params = self._set_params(record)

pysnc/record.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ def delete_multiple(self) -> bool:
706706
allRecordsWereDeleted = True
707707
def handle(response):
708708
nonlocal allRecordsWereDeleted
709-
if response.status_code != 204:
709+
if response is None or response.status_code != 204:
710710
allRecordsWereDeleted = False
711711

712712
for e in self:
@@ -716,12 +716,17 @@ def handle(response):
716716

717717
def update_multiple(self, custom_handler=None) -> bool:
718718
"""
719-
Updates multiple records at once
719+
Updates multiple records at once. A ``custom_handler`` of the form ``def handle(response: requests.Response | None)`` can be passed in,
720+
which may be useful if you wish to handle errors in a specific way. Note that if a custom_handler is used this
721+
method will always return ``True``
722+
723+
724+
:return: ``True`` on success, ``False`` if any records failed. If custom_handler is specified, always returns ``True``
720725
"""
721726
updated = True
722727
def handle(response):
723728
nonlocal updated
724-
if response.status_code != 200:
729+
if response is None or response.status_code != 200:
725730
updated = False
726731

727732
for e in self:
@@ -810,7 +815,7 @@ def set_display_value(self, field, value):
810815

811816
def set_link(self, field, value):
812817
"""
813-
Set the link for a field.
818+
Set the link for a field, it is however preferable to to `gr.field.set_link(value)`.
814819
815820
:param str field: The field
816821
:param value: The Value
@@ -823,9 +828,9 @@ def set_link(self, field, value):
823828
else:
824829
c[field].set_link(value)
825830

826-
def get_link(self, no_stack=False) -> str:
831+
def get_link(self, no_stack: bool=False) -> str:
827832
"""
828-
Generate a full URL to the current record. sys_id will be null if there is no current record.
833+
Generate a full URL to the current record. sys_id will be -1 if there is no current record.
829834
830835
:param bool no_stack: Default ``False``, adds ``&sysparm_stack=<table>_list.do?sysparm_query=active=true`` to the URL
831836
:param bool list: Default ``False``, if ``True`` then provide a link to the record set, not the current record
@@ -837,7 +842,8 @@ def get_link(self, no_stack=False) -> str:
837842
stack = '&sysparm_stack=%s_list.do?sysparm_query=active=true' % self.__table
838843
if no_stack:
839844
stack = ''
840-
id = self.sys_id if obj else 'null'
845+
id = self.sys_id if obj else None
846+
id = id or '-1'
841847
return "{}/{}.do?sys_id={}{}".format(ins, self.__table, id, stack)
842848

843849
def get_link_list(self) -> Optional[str]:
@@ -1052,6 +1058,7 @@ def serialize(self, display_value=False, fields=None, fmt=None, changes_only=Fal
10521058
:param list fields: Fields to serialize. Defaults to all fields.
10531059
:param str fmt: None or ``pandas``. Defaults to None
10541060
:param changes_only: Do we want to serialize only the fields we've modified?
1061+
:param exclude_reference_link: Do we want to exclude the reference link? default is True
10551062
:return: dict representation
10561063
"""
10571064
if fmt == 'pandas':

test/test_snc_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ def test_link_query(self):
3030
gr.query()
3131
link = gr.get_link(no_stack=True)
3232
print(link)
33-
self.assertTrue(link.endswith('sys_user.do?sys_id=null'))
33+
self.assertTrue(link.endswith('sys_user.do?sys_id=-1'))
3434
self.assertTrue(gr.next())
3535
link = gr.get_link(no_stack=True)
3636
print(link)
37-
self.assertFalse(link.endswith('sys_user.do?sys_id=null'))
37+
self.assertFalse(link.endswith('sys_user.do?sys_id=-1'))
3838
client.session.close()
3939

4040
def test_link_list(self):

test/test_snc_api_write.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,5 +254,74 @@ def custom_handle(response):
254254
tgr.delete_multiple()
255255
client.session.close()
256256

257+
def test_multi_update_with_failures(self):
258+
client = ServiceNowClient(self.c.server, self.c.credentials)
259+
br = client.GlideRecord('sys_script')
260+
261+
# in order to test the failure case, let's insert a BR that will reject specific values
262+
br.add_query('name', 'test_multi_update_with_failures')
263+
br.query()
264+
if not br.next():
265+
br.initialize()
266+
br.name = 'test_multi_update_with_failures'
267+
br.collection = 'problem'
268+
br.active = True
269+
br.when = 'before'
270+
br.order = 100
271+
br.action_insert = True
272+
br.action_update = True
273+
br.abort_action = True
274+
br.add_message = True
275+
br.message = 'rejected by test_multi_update_with_failures br'
276+
br.filter_condition = 'short_descriptionLIKEEICAR^ORdescriptionLIKEEICAR^EQ'
277+
br.insert()
278+
279+
280+
gr = client.GlideRecord('problem')
281+
gr.add_query('short_description', 'LIKE', 'BUNKZZ')
282+
gr.query()
283+
self.assertTrue(gr.delete_multiple()) # try to make sure weh ave none first
284+
gr.query()
285+
self.assertEqual(len(gr), 0, 'should have had none left')
286+
257287

288+
total_count = 10
289+
# insert five bunk records
290+
# TODO: insert multiple
291+
for i in range(total_count):
292+
gr = client.GlideRecord('problem')
293+
gr.initialize()
294+
gr.short_description = f"Unit Test - BUNKZZ Multi update {i}"
295+
assert gr.insert(), "Failed to insert a record"
296+
297+
gr = client.GlideRecord('problem')
298+
gr.add_query('short_description', 'LIKE', 'BUNKZZ')
299+
gr.query()
300+
self.assertEqual(len(gr), total_count)
301+
302+
i = 0
303+
# half append
304+
print(f"for i < {total_count//2}")
305+
while i < (total_count//2) and gr.next():
306+
gr.short_description = gr.short_description + ' -- APPENDEDZZ'
307+
i += 1
308+
# half error
309+
while gr.next():
310+
gr.short_description = gr.short_description + ' -- EICAR'
311+
312+
self.assertFalse(gr.update_multiple())
313+
# make sure we cleaned up as expected
314+
self.assertEqual(gr._client.batch_api._BatchAPI__hooks, {})
315+
self.assertEqual(gr._client.batch_api._BatchAPI__stored_requests, {})
316+
self.assertEqual(gr._client.batch_api._BatchAPI__requests, [])
317+
318+
tgr = client.GlideRecord('problem')
319+
tgr.add_query('short_description', 'LIKE', 'APPENDEDZZ')
320+
tgr.query()
321+
self.assertEqual(len(tgr), total_count//2)
322+
323+
tgr = client.GlideRecord('problem')
324+
tgr.add_query('short_description', 'LIKE', 'EICAR')
325+
tgr.query()
326+
self.assertEqual(len(tgr), 0)
258327

test/test_snc_serialization.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,13 @@ def test_serialize_reference_link(self):
168168
data = gr.serialize(exclude_reference_link=False)
169169
self.assertIsNotNone(data)
170170
self.assertEqual(gr.get_value('reffield'), 'my reference')
171-
self.assertEqual(gr.get_link('reffield'), 'https://dev00000.service-now.com/api/now/table/sys___/abcde12345')
171+
self.assertTrue(gr.get_link(True).endswith('/some_table.do?sys_id=-1'), f"was {gr.get_link()}")
172+
self.assertEqual(gr.reffield.get_link(), 'https://dev00000.service-now.com/api/now/table/sys___/abcde12345')
172173
self.assertEqual(gr.serialize(exclude_reference_link=False), {'reffield':{'value': 'my reference','link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}})
173174
self.assertEqual(data, {'reffield':{'value': 'my reference','link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}})
175+
176+
gr.reffield.set_link('https://dev00000.service-now.com/api/now/table/sys___/xyz789')
177+
self.assertEqual(gr.reffield.get_link(), 'https://dev00000.service-now.com/api/now/table/sys___/xyz789')
174178
client.session.close()
175179

176180
def test_serialize_reference_link_all(self):
@@ -181,12 +185,23 @@ def test_serialize_reference_link_all(self):
181185
gr.set_link('reffield', 'https://dev00000.service-now.com/api/now/table/sys___/abcde12345')
182186
gr.set_display_value('reffield', 'my reference display')
183187

184-
data = gr.serialize(exclude_reference_link=False)
185-
self.assertIsNotNone(data)
186188
self.assertEqual(gr.get_value('reffield'), 'my reference')
187-
self.assertEqual(gr.get_link('reffield'), 'https://dev00000.service-now.com/api/now/table/sys___/abcde12345')
188-
self.assertEqual(gr.serialize(exclude_reference_link=False), {'reffield':{'value': 'my reference','display_value': 'my reference display', 'link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}})
189-
self.assertEqual(data, {'reffield':{'value': 'my reference','display_value': 'my reference display', 'link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}})
189+
self.assertEqual(gr.get_display_value('reffield'), 'my reference display')
190+
self.assertEqual(gr.reffield.get_link(), 'https://dev00000.service-now.com/api/now/table/sys___/abcde12345')
191+
192+
self.assertEqual(gr.serialize(), {'reffield':'my reference'})
193+
self.assertEqual(
194+
gr.serialize(exclude_reference_link=False),
195+
{'reffield':{'value': 'my reference','link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}}
196+
)
197+
self.assertEqual(
198+
gr.serialize(display_value=True, exclude_reference_link=False),
199+
{'reffield':{'display_value': 'my reference display', 'link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}}
200+
)
201+
self.assertEqual(
202+
gr.serialize(display_value='both', exclude_reference_link=False),
203+
{'reffield':{'value': 'my reference','display_value': 'my reference display', 'link':'https://dev00000.service-now.com/api/now/table/sys___/abcde12345'}}
204+
)
190205
client.session.close()
191206

192207
def test_str(self):

0 commit comments

Comments
 (0)