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.
- {asyncgh-0.5.2 → asyncgh-0.5.4}/PKG-INFO +1 -1
- {asyncgh-0.5.2 → asyncgh-0.5.4}/pyproject.toml +1 -1
- {asyncgh-0.5.2 → asyncgh-0.5.4}/src/asyncgh/_version.py +2 -2
- {asyncgh-0.5.2 → asyncgh-0.5.4}/src/asyncgh/client.py +18 -2
- {asyncgh-0.5.2 → asyncgh-0.5.4}/tests/test_client.py +39 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/.gitignore +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/LICENSE +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/README.md +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/src/asyncgh/__init__.py +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/src/asyncgh/endpoints.py +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/src/asyncgh/py.typed +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/tests/conftest.py +0 -0
- {asyncgh-0.5.2 → asyncgh-0.5.4}/tests/test_endpoints.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asyncgh
|
|
3
|
-
Version: 0.5.
|
|
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
|
|
46
|
+
requires = ["hatch-vcs>=0.4", "hatchling", "setuptools-scm<9"]
|
|
47
47
|
build-backend = "hatchling.build"
|
|
48
48
|
|
|
49
49
|
[tool.coverage.run]
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|