55from requests import Request
66
77
8+ class GraphQLPaginationError (RuntimeError ):
9+ pass
10+
11+
812class GraphQLCursorPaginator (JSONResponseCursorPaginator ):
913 def __init__ (
1014 self ,
1115 page_info_path : str ,
1216 cursor_variable : str = "after" ,
1317 cursor_field : str = "endCursor" ,
1418 has_next_field : str = "hasNextPage" ,
19+ allow_missing_page_info : bool = False ,
1520 ) -> None :
1621
1722 super ().__init__ (
@@ -25,25 +30,96 @@ def __init__(
2530 self .cursor_variable = cursor_variable
2631 self .cursor_field = cursor_field
2732 self .has_next_field = has_next_field
33+ self .allow_missing_page_info = allow_missing_page_info
34+
35+ def init_request (self , request : "Request" ) -> None :
36+ self ._next_reference = None
37+ self ._has_next_page = True
2838
2939 def update_state (self , response , data = None ):
3040 response_json = response .json ()
41+ errors = response_json .get ("errors" )
42+ if errors :
43+ messages = []
44+ for error in errors :
45+ if isinstance (error , dict ):
46+ message = error .get ("message" , "unknown GraphQL error" )
47+ extensions = error .get ("extensions" ) or {}
48+ error_type = error .get ("type" ) or extensions .get ("type" )
49+ path = error .get ("path" )
50+ details = message
51+ if error_type :
52+ details = f"{ details } ({ error_type } )"
53+ if path :
54+ details = f"{ details } at { path } "
55+ messages .append (details )
56+ else :
57+ messages .append (str (error ))
58+ raise GraphQLPaginationError (
59+ f"GraphQL response contained errors while reading { self .page_info_path } : "
60+ + "; " .join (messages )
61+ )
62+
63+ self ._normalize_page_info (data )
64+
3165 page_info = jsonpath .find_values (self .page_info_path , response_json )
3266
3367 if not page_info :
68+ if not self .allow_missing_page_info :
69+ raise GraphQLPaginationError (
70+ f"GraphQL pageInfo not found at { self .page_info_path } "
71+ )
3472 self ._next_reference = None
3573 self ._has_next_page = False
3674 return
3775
3876 page_info_obj = page_info [0 ]
3977 cursor = page_info_obj .get (self .cursor_field )
4078 has_next = page_info_obj .get (self .has_next_field )
79+
80+ if not isinstance (has_next , bool ):
81+ raise GraphQLPaginationError (
82+ f"GraphQL { self .page_info_path } .{ self .has_next_field } must be a bool"
83+ )
84+ if has_next and not cursor :
85+ raise GraphQLPaginationError (
86+ f"GraphQL { self .page_info_path } .{ self .cursor_field } is required when "
87+ f"{ self .has_next_field } is true"
88+ )
89+
4190 self ._next_reference = cursor
4291 self ._has_next_page = has_next
4392
93+ def _normalize_page_info (self , data ):
94+ if isinstance (data , list ):
95+ for item in data :
96+ self ._normalize_page_info (item )
97+ return
98+
99+ if not isinstance (data , dict ):
100+ return
101+
102+ page_info = data .get ("pageInfo" )
103+ if isinstance (page_info , dict ):
104+ has_next = page_info .get (self .has_next_field )
105+ if has_next is False and self .cursor_field not in page_info :
106+ page_info [self .cursor_field ] = None
107+
108+ for value in data .values ():
109+ self ._normalize_page_info (value )
110+
44111 def update_request (self , request : "Request" ) -> None :
45- if not self ._next_reference or not hasattr ( request , "json" ) :
112+ if not self ._has_next_page :
46113 return
47114
48- if isinstance (request .json , dict ) and "variables" in request .json :
49- request .json ["variables" ][self .cursor_variable ] = self ._next_reference
115+ if not self ._next_reference :
116+ raise GraphQLPaginationError (
117+ f"GraphQL cursor is missing for variable { self .cursor_variable } "
118+ )
119+ if not isinstance (request .json , dict ):
120+ raise GraphQLPaginationError ("GraphQL request body must be a JSON object" )
121+ variables = request .json .get ("variables" )
122+ if not isinstance (variables , dict ):
123+ raise GraphQLPaginationError ("GraphQL request body must contain variables" )
124+
125+ variables [self .cursor_variable ] = self ._next_reference
0 commit comments