Skip to content

Commit 086ff99

Browse files
Stabilize high level scan zip upload and page iterator (#9)
* bump requests * better page load fail handling * handle upload failures better
1 parent a848efd commit 086ff99

File tree

4 files changed

+62
-20
lines changed

4 files changed

+62
-20
lines changed

.vscode/launch.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"request": "launch",
1111
"program": "${file}",
1212
"console": "integratedTerminal",
13-
"env": { "PYTHONPATH" : "${workspaceFolder}:${workspaceFolder}/tests"}
13+
"env": { "PYTHONPATH" : "${workspaceFolder}:${workspaceFolder}/tests"},
14+
"envFile": "${workspaceFolder}/.env"
1415
}
1516
]
1617
}

cxone_api/high/scans.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ..low.scan_configuration import retrieve_tenant_configuration
1111
from ..low.uploads import generate_upload_link, upload_to_link
1212
from .projects import ProjectRepoConfig
13+
import asyncio, logging
1314

1415

1516
class ScanInvoker:
@@ -96,12 +97,31 @@ async def scan_get_scanid(cxone_client : CxOneClient, project_repo : ProjectRepo
9697

9798

9899
@staticmethod
99-
async def __upload_zip(cxone_client : CxOneClient, zip_path : str) -> str:
100-
upload_url = json_on_ok(await generate_upload_link(cxone_client))['url']
100+
async def __upload_zip(cxone_client : CxOneClient, zip_path : str, max_retries : int = 5, retry_delay_s : int = 3) -> str:
101+
_log = logging.getLogger("ScanInvoker.__upload_zip")
102+
103+
upload_url = None
104+
retries = 0
105+
while retries < max_retries:
106+
if not retries == 0:
107+
await asyncio.sleep(retry_delay_s)
108+
retries += 1
109+
110+
link_response = await generate_upload_link(cxone_client)
111+
if link_response.ok:
112+
upload_url = link_response.json()['url']
113+
else:
114+
_log.debug(f"Failed to generate link: {link_response.status_code} {link_response.text}")
115+
continue
116+
117+
upload_response = await upload_to_link(cxone_client, upload_url, zip_path)
118+
if not upload_response.ok:
119+
_log.debug(f"Failed to upload zip: {link_response.status_code} {link_response.text}")
120+
upload_url = None
121+
continue
101122

102-
upload_response = await upload_to_link(cxone_client, upload_url, zip_path)
103-
if not upload_response.ok:
104-
return None
123+
if upload_url is None:
124+
raise ScanException("Failed to upload the zip for scan.")
105125

106126
return upload_url
107127

cxone_api/util.py

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import re, urllib, requests
1+
import re, urllib, requests, logging, functools, inspect, asyncio
22
from datetime import datetime
33
from requests import Response
44
from requests.compat import urljoin
@@ -83,6 +83,8 @@ def normalized_string(s):
8383

8484

8585
def decorator(wrapped):
86+
87+
@functools.wraps(wrapped)
8688
async def wrapper(*inner_args, **inner_kwargs):
8789

8890
normalized = {}
@@ -113,7 +115,8 @@ async def wrapper(*inner_args, **inner_kwargs):
113115

114116

115117

116-
async def page_generator(coro, array_element=None, offset_param='offset', offset_init_value=0, offset_is_by_count=True, **kwargs):
118+
async def page_generator(coro, array_element=None, offset_param='offset', offset_init_value=0, offset_is_by_count=True,
119+
page_retries_max=5, page_retry_delay_s=3, **kwargs):
117120
"""
118121
An async generator function that is used to automatically fetch the next page
119122
of results from the API when the API supports result paging.
@@ -128,24 +131,42 @@ async def page_generator(coro, array_element=None, offset_param='offset', offset
128131
offset_is_by_count - Set to true (default) if the API next offset is indicated by count of elements retrieved.
129132
If set to false, the offset is incremented by one to indicate a page offset where the count
130133
of results per page is set by other parameters.
134+
page_retries_max - The number of retries to fetch a page in the event of an error. Defaults to 5.
135+
page_retry_delay_s - The number of seconds to delay the next page fetch retry. Defaults to 3 seconds.
136+
131137
kwargs - Keyword args passed to the coroutine at the time the coroutine is executed.
132138
"""
139+
_log = logging.getLogger(f"page_generator:{inspect.unwrap(coro).__name__}")
140+
133141
offset = offset_init_value
134142
buf = []
143+
retries = 0
144+
135145

136146
while True:
137147
if len(buf) == 0:
138-
kwargs[offset_param] = offset
139-
json = (await coro(**kwargs)).json()
140-
buf = json[array_element] if array_element is not None else json
141-
142-
if buf is None or len(buf) == 0:
143-
return
144-
145-
if offset_is_by_count:
146-
offset = offset + len(buf)
147-
else:
148-
offset += 1
148+
try:
149+
kwargs[offset_param] = offset
150+
json = (await coro(**kwargs)).json()
151+
buf = json[array_element] if array_element is not None else json
152+
retries = 0
153+
154+
if buf is None or len(buf) == 0:
155+
return
156+
157+
if offset_is_by_count:
158+
offset = offset + len(buf)
159+
else:
160+
offset += 1
161+
except BaseException as ex:
162+
if retries < page_retries_max:
163+
_log.debug(f"Exception fetching next page, will retry: {ex}")
164+
await asyncio.sleep(page_retry_delay_s)
165+
retries += 1
166+
continue
167+
else:
168+
_log.debug(f"Abort after {retries} retries", ex)
169+
raise
149170

150171
yield buf.pop()
151172

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
66
name = "cxone_api"
77
dynamic = ["version"]
88
dependencies = [
9-
"requests==2.32.3",
9+
"requests==2.32.4",
1010
"jsonpath_ng==1.7.0",
1111
"validators==0.34.0",
1212
"dataclasses-json==0.6.7",

0 commit comments

Comments
 (0)