asyncgh 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: asyncgh
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Minimal async GitHub REST + GraphQL transport: auth, retry, pagination -- no generated models
5
5
  Project-URL: Homepage, https://github.com/hugoh/gh-workflows/tree/main/asyncgh
6
6
  Project-URL: Issues, https://github.com/hugoh/gh-workflows/issues
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "asyncgh"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "Minimal async GitHub REST + GraphQL transport: auth, retry, pagination -- no generated models"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -106,6 +106,22 @@ def _should_retry_graphql(exc: Exception) -> bool | float:
106
106
  return _should_retry(exc)
107
107
 
108
108
 
109
+ def _format_graphql_errors(errors: list[dict]) -> str:
110
+ """Joins GraphQL `errors[]` into one message, appending each error's
111
+ `path` (e.g. `r3.pullRequests.nodes.0.commits`) when present -- a
112
+ partial-permission failure names only "Resource not accessible by
113
+ personal access token" otherwise, with no hint which repo or field.
114
+ """
115
+ parts = []
116
+ for error in errors:
117
+ message = error.get("message", str(error))
118
+ path = error.get("path")
119
+ if path:
120
+ message = f"{message} (at {'.'.join(str(segment) for segment in path)})"
121
+ parts.append(message)
122
+ return "; ".join(parts)
123
+
124
+
109
125
  def error_message(response: httpx2.Response) -> str:
110
126
  """Extracts GitHub's own `message` field from an error response body,
111
127
  falling back to the raw response text if the body isn't JSON, or isn't
@@ -294,13 +310,9 @@ class GitHubClient:
294
310
  if not errors:
295
311
  return body["data"]
296
312
  if any(error.get("type") == "RATE_LIMITED" for error in errors):
297
- raise _GraphQLRateLimited(
298
- "; ".join(
299
- error.get("message", str(error)) for error in errors
300
- )
301
- )
313
+ raise _GraphQLRateLimited(_format_graphql_errors(errors))
302
314
  raise GhError(
303
- "; ".join(error.get("message", str(error)) for error in errors),
315
+ _format_graphql_errors(errors),
304
316
  error_type=errors[0].get("type"),
305
317
  )
306
318
  except _GraphQLRateLimited as exc:
@@ -244,6 +244,32 @@ async def test_graphql_raises_gh_error_when_errors_present_alongside_partial_dat
244
244
  await graphql("query { r0 r1 }")
245
245
 
246
246
 
247
+ async def test_graphql_error_message_includes_the_error_path(
248
+ httpx2_mock: respx.Router,
249
+ ):
250
+ httpx2_mock.post(f"{API_BASE}/graphql").mock(
251
+ return_value=httpx.Response(
252
+ 200,
253
+ json={
254
+ "data": {"r3": None},
255
+ "errors": [
256
+ {
257
+ "type": "FORBIDDEN",
258
+ "message": "Resource not accessible by personal access token",
259
+ "path": ["r3", "pullRequests", "nodes", 0, "commits"],
260
+ }
261
+ ],
262
+ },
263
+ )
264
+ )
265
+ with pytest.raises(
266
+ GhError,
267
+ match=r"personal access token \(at r3\.pullRequests\.nodes\.0\.commits\)",
268
+ ) as exc_info:
269
+ await graphql("query { r3 { pullRequests { nodes { commits } } } }")
270
+ assert exc_info.value.error_type == "FORBIDDEN"
271
+
272
+
247
273
  async def test_graphql_retries_on_transport_error(httpx2_mock: respx.Router):
248
274
  route = httpx2_mock.post(f"{API_BASE}/graphql").mock(
249
275
  side_effect=[
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes