Skip to content

Commit 675415a

Browse files
committed
Modified helper to deal with GraphQL errors
1 parent 53aab47 commit 675415a

3 files changed

Lines changed: 83 additions & 5 deletions

File tree

src/openhound_github/helpers.py

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@
55
from requests import Request
66

77

8+
class GraphQLPaginationError(RuntimeError):
9+
pass
10+
11+
812
class 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

src/openhound_github/models/team.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class MemberEdge(BaseModel):
5757

5858

5959
class PageInfo(BaseModel):
60-
end_cursor: str = Field(alias="endCursor")
60+
end_cursor: str | None = Field(alias="endCursor")
6161
has_next_page: bool = Field(alias="hasNextPage")
6262

6363

@@ -95,7 +95,7 @@ class Team(BaseAsset):
9595

9696
name: str
9797
id: str
98-
database_id: int = Field(alias="databaseId")
98+
database_id: int | None = Field(alias="databaseId", default=None)
9999
slug: str
100100
description: str | None = None
101101
privacy: str | None = None

src/openhound_github/resources/enterprise.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ def enterprise_saml_provider(enterprise_data: Enterprise, ctx: SourceContext):
322322
cursor_variable="after",
323323
cursor_field="endCursor",
324324
has_next_field="hasNextPage",
325+
allow_missing_page_info=True,
325326
)
326327
data = {
327328
"query": ENTERPRISE_SAML_QUERY,
@@ -361,6 +362,7 @@ def enterprise_external_identities(
361362
cursor_variable="after",
362363
cursor_field="endCursor",
363364
has_next_field="hasNextPage",
365+
allow_missing_page_info=True,
364366
)
365367
data = {
366368
"query": ENTERPRISE_SAML_QUERY,

0 commit comments

Comments
 (0)