Skip to content

Commit ada07dd

Browse files
authored
Merge pull request #111 from ServiceNow/scratch/iter
feat: non-rewindable
2 parents b1ffe21 + c31b7fd commit ada07dd

File tree

6 files changed

+193
-173
lines changed

6 files changed

+193
-173
lines changed

poetry.lock

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

pyproject.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@ classifiers = [
1818

1919
[tool.poetry.dependencies]
2020
python = "^3.8"
21-
requests = "2.31.0"
21+
requests = "^2.31.0"
2222
requests-oauthlib = { version = ">=1.2.0", optional = true}
23-
certifi = "2023.7.22"
24-
pip = ">=23.3.1"
23+
certifi = "^2024.7.4"
2524
urllib3 = "^2.0.7"
2625

2726
[tool.poetry.extras]

pysnc/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,19 @@ def __init__(self, instance, auth, proxy=None, verify=None, cert=None, auto_retr
8383
self.attachment_api = AttachmentAPI(self)
8484
self.batch_api = BatchAPI(self)
8585

86-
def GlideRecord(self, table, batch_size=100) -> GlideRecord:
86+
def GlideRecord(self, table, batch_size=100, rewindable=True) -> GlideRecord:
8787
"""
8888
Create a :class:`pysnc.GlideRecord` for a given table against the current client
8989
9090
:param str table: The table name e.g. ``problem``
9191
:param int batch_size: Batch size (items returned per HTTP request). Default is ``100``.
92+
:param bool rewindable: If we can rewind the record. Default is ``True``. If ``False`` then we cannot rewind
93+
the record, which means as an Iterable this object will be 'spent' after iteration.
94+
This is normally the default behavior expected for a python Iterable, but not a GlideRecord.
95+
When ``False`` less memory will be consumed, as each previous record will be collected.
9296
:return: :class:`pysnc.GlideRecord`
9397
"""
94-
return GlideRecord(self, table, batch_size)
98+
return GlideRecord(self, table, batch_size, rewindable)
9599

96100
def Attachment(self, table) -> Attachment:
97101
"""

pysnc/record.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,12 @@ class GlideRecord(object):
243243
:param ServiceNowClient client: We need to know which instance we're connecting to
244244
:param str table: The table are we going to access
245245
:param int batch_size: Batch size (items returned per HTTP request). Default is ``500``.
246+
:param bool rewindable: If we can rewind the record. Default is ``True``. If ``False`` then we cannot rewind
247+
the record, which means as an Iterable this object will be 'spent' after iteration.
248+
This is normally the default behavior expected for a python Iterable, but not a GlideRecord.
249+
When ``False`` less memory will be consumed, as each previous record will be collected.
246250
"""
247-
def __init__(self, client: 'ServiceNowClient', table: str, batch_size=500):
251+
def __init__(self, client: 'ServiceNowClient', table: str, batch_size: int=500, rewindable: bool=True):
248252
self._log = logging.getLogger(__name__)
249253
self._client = client
250254
self.__table: str = table
@@ -262,6 +266,7 @@ def __init__(self, client: 'ServiceNowClient', table: str, batch_size=500):
262266
self.__order: str = "ORDERBYsys_id" # we *need* a default order in the event we page, see issue#96
263267
self.__is_new_record: bool = False
264268
self.__display_value: Union[bool, str] = 'all'
269+
self.__rewindable = rewindable
265270

266271
def _clear_query(self):
267272
self.__query = Query(self.__table)
@@ -443,6 +448,7 @@ def order_by_desc(self, column: str):
443448
def pop_record(self) -> 'GlideRecord':
444449
"""
445450
Pop the current record into a new :class:`GlideRecord` object - equivalent to a clone of a singular record
451+
FIXME: this, by the name, should be a destructive operation, but it is not.
446452
447453
:return: Give us a new :class:`GlideRecord` containing only the current record
448454
"""
@@ -481,8 +487,10 @@ def set_new_guid_value(self, value):
481487

482488
def rewind(self):
483489
"""
484-
Rewinds the record so it may be iterated upon again. Not required to be called if iterating in the pythonic method.
490+
Rewinds the record (iterable) so it may be iterated upon again. Only possible when the record is rewindable.
485491
"""
492+
if not self._is_rewindable():
493+
raise Exception('Cannot rewind a non-rewindable record')
486494
self.__current = -1
487495

488496
def changes(self) -> bool:
@@ -506,6 +514,12 @@ def query(self, query=None):
506514
:AuthenticationException: If we do not have rights
507515
:RequestException: If the transaction is canceled due to execution time
508516
"""
517+
if not self._is_rewindable() and self.__current > 0:
518+
raise RuntimeError(f"huh {self._is_rewindable} and {self.__current}")
519+
# raise RuntimeError('Cannot re-query a non-rewindable record that has been iterated upon')
520+
self._do_query(query)
521+
522+
def _do_query(self, query=None):
509523
stored = self.__query
510524
if query:
511525
assert isinstance(query, Query), 'cannot query with a non query object'
@@ -564,7 +578,7 @@ def get(self, name, value=None) -> bool:
564578
return False
565579
else:
566580
self.add_query(name, value)
567-
self.query()
581+
self._do_query()
568582
return self.next()
569583

570584
def insert(self) -> Optional[GlideElement]:
@@ -645,7 +659,7 @@ def delete_multiple(self) -> bool:
645659
if self.__total is None:
646660
if not self.__field_limits:
647661
self.fields = 'sys_id' # type: ignore ## all we need...
648-
self.query()
662+
self._do_query()
649663

650664
allRecordsWereDeleted = True
651665
def handle(response):
@@ -1074,9 +1088,13 @@ def to_pandas(self, columns=None, mode='smart'):
10741088

10751089
return data
10761090

1091+
def _is_rewindable(self) -> bool:
1092+
return self.__rewindable
1093+
10771094
def __iter__(self):
10781095
self.__is_iter = True
1079-
self.rewind()
1096+
if self._is_rewindable():
1097+
self.rewind()
10801098
return self
10811099

10821100
def __next__(self):
@@ -1092,6 +1110,8 @@ def next(self, _recursive=False) -> bool:
10921110
if l > 0 and self.__current+1 < l:
10931111
self.__current = self.__current + 1
10941112
if self.__is_iter:
1113+
if not self._is_rewindable(): # if we're not rewindable, remove the previous record
1114+
self.__results[self.__current - 1] = None
10951115
return self # type: ignore # this typing is internal only
10961116
return True
10971117
if self.__total and self.__total > 0 and \
@@ -1100,10 +1120,10 @@ def next(self, _recursive=False) -> bool:
11001120
_recursive is False:
11011121
if self.__limit:
11021122
if self.__current+1 < self.__limit:
1103-
self.query()
1123+
self._do_query()
11041124
return self.next(_recursive=True)
11051125
else:
1106-
self.query()
1126+
self._do_query()
11071127
return self.next(_recursive=True)
11081128
if self.__is_iter:
11091129
self.__is_iter = False

test/test_snc_auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest import TestCase
1+
from unittest import TestCase, skip
22

33
from pysnc import ServiceNowClient
44
from pysnc.auth import *
@@ -29,6 +29,7 @@ def test_basic_fail(self):
2929
except Exception:
3030
assert 'Should have got an Auth exception'
3131

32+
@skip("Requires valid oauth client_id and secret, and I don't want to need anything not out of box")
3233
def test_oauth(self):
3334
# Manual setup using legacy oauth
3435
server = self.c.server

test/test_snc_iteration.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from unittest import TestCase
2+
3+
from pysnc import ServiceNowClient
4+
from constants import Constants
5+
from pprint import pprint
6+
7+
class TestIteration(TestCase):
8+
c = Constants()
9+
10+
def test_default_behavior(self):
11+
client = ServiceNowClient(self.c.server, self.c.credentials)
12+
gr = client.GlideRecord('sys_metadata', batch_size=100)
13+
gr.fields = 'sys_id'
14+
gr.limit = 500
15+
gr.query()
16+
self.assertTrue(gr._is_rewindable())
17+
18+
self.assertTrue(len(gr) > 500, 'Expected more than 500 records')
19+
20+
count = 0
21+
while gr.next():
22+
count += 1
23+
self.assertEqual(count, 500, 'Expected 500 records when using next')
24+
25+
self.assertEqual(len([r.sys_id for r in gr]), 500, 'Expected 500 records when an iterable')
26+
self.assertEqual(len([r.sys_id for r in gr]), 500, 'Expected 500 records when iterated again')
27+
28+
# expect the same for next
29+
count = 0
30+
while gr.next():
31+
count += 1
32+
self.assertEqual(count, 0, 'Expected 0 records when not rewound, as next does not auto-rewind')
33+
gr.rewind()
34+
while gr.next():
35+
count += 1
36+
self.assertEqual(count, 500, 'Expected 500 post rewind')
37+
38+
# should not throw
39+
gr.query()
40+
gr.query()
41+
42+
client.session.close()
43+
44+
def test_rewind_behavior(self):
45+
client = ServiceNowClient(self.c.server, self.c.credentials)
46+
gr = client.GlideRecord('sys_metadata', batch_size=250, rewindable=False)
47+
gr.fields = 'sys_id'
48+
gr.limit = 500
49+
gr.query()
50+
self.assertEqual(gr._GlideRecord__current, -1)
51+
self.assertFalse(gr._is_rewindable())
52+
self.assertEqual(len([r for r in gr]), 500, 'Expected 500 records when an iterable')
53+
self.assertEqual(len([r for r in gr]), 0, 'Expected no records when iterated again')
54+
55+
# but if we query again...
56+
with self.assertRaises(RuntimeError):
57+
gr.query()

0 commit comments

Comments
 (0)