asyncgh 0.5.2__tar.gz → 0.5.4__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.5.2
3
+ Version: 0.5.4
4
4
  Summary: Minimal async GitHub REST + GraphQL transport: auth, retry, pagination -- no generated models
5
5
  Project-URL: Homepage, https://github.com/hugoh/fleet-tools/tree/main/asyncgh
6
6
  Project-URL: Issues, https://github.com/hugoh/fleet-tools/issues
@@ -43,7 +43,7 @@ dev = [
43
43
  # still forwards raw-options as get_version() kwargs, so under v10 `tag` errors
44
44
  # and the deprecated `git_describe_command` --match is ignored (wrong tag picked).
45
45
  # Unpin once hatch-vcs ships setuptools-scm 10 support.
46
- requires = ["hatch-vcs>=0.4", "hatchling", "setuptools-scm>=8,<9"]
46
+ requires = ["hatch-vcs>=0.4", "hatchling", "setuptools-scm<9"]
47
47
  build-backend = "hatchling.build"
48
48
 
49
49
  [tool.coverage.run]
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.5.2'
21
- __version_tuple__ = version_tuple = (0, 5, 2)
20
+ __version__ = version = '0.5.4'
21
+ __version_tuple__ = version_tuple = (0, 5, 4)
@@ -119,9 +119,18 @@ class _GraphQLRateLimited(Exception):
119
119
  """
120
120
 
121
121
 
122
+ class _GraphQLInvalidBody(Exception):
123
+ """Internal marker: GraphQL answered 2xx with a body that isn't JSON
124
+ (typically empty, from a gateway giving up on an expensive query).
125
+ Transient, so retried on the default backoff.
126
+ """
127
+
128
+
122
129
  def _should_retry_graphql(exc: Exception) -> bool | float:
123
130
  if isinstance(exc, _GraphQLRateLimited):
124
131
  return SECONDARY_RATE_LIMIT_WAIT
132
+ if isinstance(exc, _GraphQLInvalidBody):
133
+ return True
125
134
  return _should_retry(exc)
126
135
 
127
136
 
@@ -324,7 +333,14 @@ class GitHubClient:
324
333
  raise GhError(
325
334
  error_message(response), status_code=response.status_code
326
335
  )
327
- body = response.json()
336
+ try:
337
+ body = response.json()
338
+ except ValueError as exc:
339
+ raise _GraphQLInvalidBody(
340
+ "GraphQL response body is not valid JSON "
341
+ f"(status {response.status_code}, "
342
+ f"body: {response.text[:200]!r}): {exc}"
343
+ ) from exc
328
344
  errors = body.get("errors")
329
345
  if not errors:
330
346
  return body["data"]
@@ -334,7 +350,7 @@ class GitHubClient:
334
350
  _format_graphql_errors(errors),
335
351
  error_type=errors[0].get("type"),
336
352
  )
337
- except _GraphQLRateLimited as exc:
353
+ except (_GraphQLRateLimited, _GraphQLInvalidBody) as exc:
338
354
  raise GhError(str(exc)) from exc
339
355
  except httpx2.HTTPStatusError as exc:
340
356
  raise GhError(
@@ -499,3 +499,42 @@ async def test_github_client_as_context_manager_closes_on_exit(
499
499
  http = gh._http
500
500
  assert http is not None
501
501
  assert http.is_closed
502
+
503
+
504
+ async def test_graphql_retries_undecodable_success_body_then_succeeds(
505
+ httpx2_mock: respx.Router,
506
+ ):
507
+ route = httpx2_mock.post(f"{API_BASE}/graphql").mock(
508
+ side_effect=[
509
+ httpx.Response(200, content=b""),
510
+ httpx.Response(200, json={"data": {"ok": True}}),
511
+ ]
512
+ )
513
+ data = await graphql("query { ok }")
514
+ assert data == {"ok": True}
515
+ assert route.call_count == 2
516
+
517
+
518
+ async def test_graphql_raises_gh_error_when_success_body_stays_undecodable(
519
+ httpx2_mock: respx.Router,
520
+ ):
521
+ route = httpx2_mock.post(f"{API_BASE}/graphql").mock(
522
+ return_value=httpx.Response(200, content=b"")
523
+ )
524
+ with pytest.raises(GhError, match="not valid JSON"):
525
+ await graphql("query { ok }")
526
+ assert route.call_count == client.DEFAULT_MAX_RETRIES + 1
527
+
528
+
529
+ async def test_graphql_invalid_body_error_includes_status_and_body_prefix(
530
+ httpx2_mock: respx.Router,
531
+ ):
532
+ httpx2_mock.post(f"{API_BASE}/graphql").mock(
533
+ return_value=httpx.Response(200, content=b"<html>" + b"x" * 500)
534
+ )
535
+ with pytest.raises(GhError) as excinfo:
536
+ await graphql("query { ok }")
537
+ message = str(excinfo.value)
538
+ assert "status 200" in message
539
+ assert "<html>xxx" in message
540
+ assert "x" * 300 not in message
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes