Skip to content

Commit ca14fd3

Browse files
authored
Merge pull request #45 from JupiterOne/KNO-540
add query_with_deferred_response method
2 parents 8f56521 + a49b99b commit ca14fd3

File tree

2 files changed

+108
-10
lines changed

2 files changed

+108
-10
lines changed

jupiterone/client.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
UPDATE_RELATIONSHIPV2,
3232
DELETE_RELATIONSHIP,
3333
CURSOR_QUERY_V1,
34+
DEFERRED_RESPONSE_QUERY,
3435
CREATE_INSTANCE,
3536
INTEGRATION_JOB_VALUES,
3637
INTEGRATION_INSTANCE_EVENT_VALUES,
@@ -265,6 +266,86 @@ def _limit_and_skip_query(
265266
page += 1
266267

267268
return {"data": results}
269+
270+
def query_with_deferred_response(self, query, cursor=None):
271+
"""
272+
Execute a J1QL query that returns a deferred response for handling large result sets.
273+
274+
Args:
275+
query (str): The J1QL query to execute
276+
cursor (str, optional): Pagination cursor for subsequent requests
277+
278+
Returns:
279+
list: Combined results from all paginated responses
280+
"""
281+
all_query_results = []
282+
current_cursor = cursor
283+
284+
while True:
285+
variables = {
286+
"query": query,
287+
"deferredResponse": "FORCE",
288+
"cursor": current_cursor,
289+
"flags": {"variableResultSize": True}
290+
}
291+
292+
payload = {
293+
"query": DEFERRED_RESPONSE_QUERY,
294+
"variables": variables
295+
}
296+
297+
# Use session with retries for reliability
298+
max_retries = 5
299+
backoff_factor = 2
300+
301+
for attempt in range(1, max_retries + 1):
302+
303+
session = requests.Session()
304+
retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504, 429])
305+
session.mount('https://', HTTPAdapter(max_retries=retries))
306+
307+
# Get the download URL
308+
url_response = session.post(
309+
self.graphql_url,
310+
headers=self.headers,
311+
json=payload,
312+
timeout=60
313+
)
314+
315+
if url_response.status_code == 429:
316+
retry_after = int(url_response.headers.get("Retry-After", backoff_factor ** attempt))
317+
print(f"Rate limited. Retrying in {retry_after} seconds...")
318+
time.sleep(retry_after)
319+
else:
320+
break # Exit on success or other non-retryable error
321+
322+
if url_response.ok:
323+
324+
download_url = url_response.json()['data']['queryV1']['url']
325+
326+
# Poll the download URL until results are ready
327+
while True:
328+
download_response = session.get(download_url, timeout=60).json()
329+
status = download_response['status']
330+
331+
if status != 'IN_PROGRESS':
332+
break
333+
334+
time.sleep(0.2) # Sleep 200 milliseconds between checks
335+
336+
# Add results to the collection
337+
all_query_results.extend(download_response['data'])
338+
339+
# Check for more pages
340+
if 'cursor' in download_response:
341+
current_cursor = download_response['cursor']
342+
else:
343+
break
344+
345+
else:
346+
print(f"Request failed after {max_retries} attempts. Status: {url_response.status_code}")
347+
348+
return all_query_results
268349

269350
def _execute_syncapi_request(self, endpoint: str, payload: Dict = None) -> Dict:
270351
"""Executes POST request to SyncAPI endpoints"""

jupiterone/constants.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@
131131
entityRawDataLegacy(entityId: $entityId, , source: $source) {
132132
entityId
133133
payload {
134-
135134
... on RawDataJSONEntityLegacy {
136135
contentType
137136
name
@@ -360,7 +359,7 @@
360359
...IntegrationDefinitionConfigFragment @include(if: $includeConfig)
361360
__typename
362361
}
363-
362+
364363
fragment IntegrationDefinitionConfigFragment on IntegrationDefinition {
365364
configFields {
366365
...ConfigFieldsRecursive
@@ -387,7 +386,7 @@
387386
}
388387
__typename
389388
}
390-
389+
391390
fragment ConfigFieldsRecursive on ConfigField {
392391
...ConfigFieldValues
393392
configFields {
@@ -400,7 +399,7 @@
400399
}
401400
__typename
402401
}
403-
402+
404403
fragment ConfigFieldValues on ConfigField {
405404
key
406405
displayName
@@ -450,7 +449,7 @@
450449
}
451450
__typename
452451
}
453-
452+
454453
query IntegrationInstances($definitionId: String, $cursor: String, $limit: Int, $filter: ListIntegrationInstancesSearchFilter) {
455454
integrationInstancesV2(
456455
definitionId: $definitionId
@@ -507,7 +506,7 @@
507506
collectorPoolId
508507
__typename
509508
}
510-
509+
511510
fragment IntegrationInstanceJobValues on IntegrationJob {
512511
id
513512
status
@@ -517,7 +516,7 @@
517516
hasSkippedSteps
518517
__typename
519518
}
520-
519+
521520
query IntegrationInstance($integrationInstanceId: String!) {
522521
integrationInstance(id: $integrationInstanceId) {
523522
...IntegrationInstanceValues
@@ -577,6 +576,24 @@
577576
}
578577
}
579578
"""
579+
DEFERRED_RESPONSE_QUERY = """
580+
query J1QL(
581+
$query: String!
582+
$variables: JSON
583+
$cursor: String
584+
$deferredResponse: DeferredResponseOption
585+
) {
586+
queryV1(
587+
query: $query
588+
variables: $variables
589+
deferredResponse: $deferredResponse
590+
cursor: $cursor
591+
) {
592+
type
593+
url
594+
}
595+
}
596+
"""
580597
J1QL_FROM_NATURAL_LANGUAGE = """
581598
query j1qlFromNaturalLanguage($input: J1qlFromNaturalLanguageInput!) {
582599
j1qlFromNaturalLanguage(input: $input) {
@@ -660,7 +677,7 @@
660677
__typename
661678
}
662679
}
663-
680+
664681
fragment RuleInstanceFields on QuestionRuleInstance {
665682
id
666683
accountId
@@ -726,7 +743,7 @@
726743
__typename
727744
}
728745
}
729-
746+
730747
fragment RuleInstanceFields on QuestionRuleInstance {
731748
id
732749
accountId
@@ -869,7 +886,7 @@
869886
__typename
870887
}
871888
}
872-
889+
873890
fragment QuestionFields on Question {
874891
id
875892
sourceId

0 commit comments

Comments
 (0)