pytest-graphql 0.1.0a1__py3-none-any.whl
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.
- pytest_graphql/__init__.py +62 -0
- pytest_graphql/_core/__init__.py +7 -0
- pytest_graphql/_core/auth.py +89 -0
- pytest_graphql/_core/client.py +921 -0
- pytest_graphql/_core/diagnostics.py +2352 -0
- pytest_graphql/_core/errors.py +551 -0
- pytest_graphql/_core/headers.py +40 -0
- pytest_graphql/_core/lifecycle.py +1119 -0
- pytest_graphql/_core/middleware.py +73 -0
- pytest_graphql/_core/naming.py +107 -0
- pytest_graphql/_core/operation.py +464 -0
- pytest_graphql/_core/response/__init__.py +23 -0
- pytest_graphql/_core/response/envelope.py +158 -0
- pytest_graphql/_core/response/materialize.py +418 -0
- pytest_graphql/_core/response/node.py +221 -0
- pytest_graphql/_core/response/types.py +118 -0
- pytest_graphql/_core/schema/__init__.py +3 -0
- pytest_graphql/_core/schema/cache.py +32 -0
- pytest_graphql/_core/schema/info.py +56 -0
- pytest_graphql/_core/schema/source.py +112 -0
- pytest_graphql/_core/selection/__init__.py +11 -0
- pytest_graphql/_core/selection/builder.py +822 -0
- pytest_graphql/_core/selection/model.py +202 -0
- pytest_graphql/_core/selection/normalize.py +1423 -0
- pytest_graphql/_core/selection/policy.py +261 -0
- pytest_graphql/_core/transport/__init__.py +10 -0
- pytest_graphql/_core/transport/base.py +79 -0
- pytest_graphql/_core/transport/httpx_transport.py +1020 -0
- pytest_graphql/_core/validation.py +147 -0
- pytest_graphql/plugin/__init__.py +143 -0
- pytest_graphql/py.typed +0 -0
- pytest_graphql-0.1.0a1.dist-info/METADATA +120 -0
- pytest_graphql-0.1.0a1.dist-info/RECORD +36 -0
- pytest_graphql-0.1.0a1.dist-info/WHEEL +4 -0
- pytest_graphql-0.1.0a1.dist-info/entry_points.txt +2 -0
- pytest_graphql-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Schema-aware GraphQL test client with an optional pytest plugin.
|
|
2
|
+
|
|
3
|
+
The public API is re-exported from this module. Per C9, only names a user
|
|
4
|
+
constructs, catches, annotates or calls belong in ``__all__``; a name lands
|
|
5
|
+
here in the milestone that implements it, and the rest of the documented
|
|
6
|
+
surface arrives with the milestones that still owe it. ``__version__`` is the
|
|
7
|
+
single source of the distribution version, and the release workflow checks
|
|
8
|
+
it against the tag.
|
|
9
|
+
|
|
10
|
+
Importing this module imports no pytest (C41). The pytest layer lives under
|
|
11
|
+
``pytest_graphql.plugin`` and nothing here reaches into it.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pytest_graphql._core.auth import Auth, BearerAuth, HeaderAuth
|
|
17
|
+
from pytest_graphql._core.client import (
|
|
18
|
+
ClientConfig,
|
|
19
|
+
GraphQLClient,
|
|
20
|
+
build_client,
|
|
21
|
+
)
|
|
22
|
+
from pytest_graphql._core.diagnostics import DiagnosticSnapshot, RequestInfo
|
|
23
|
+
from pytest_graphql._core.errors import (
|
|
24
|
+
DiagnosticRenderError,
|
|
25
|
+
GraphQLFieldError,
|
|
26
|
+
ResponseShapeError,
|
|
27
|
+
)
|
|
28
|
+
from pytest_graphql._core.middleware import BaseMiddleware, Middleware
|
|
29
|
+
from pytest_graphql._core.response import GraphQLResponse, Node, NodeList
|
|
30
|
+
from pytest_graphql._core.schema.source import SchemaSource
|
|
31
|
+
from pytest_graphql._core.selection.model import AUTO, Field, Selection
|
|
32
|
+
from pytest_graphql._core.selection.policy import CyclePolicy, SelectionPolicy
|
|
33
|
+
from pytest_graphql._core.transport.base import Transport
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AUTO",
|
|
37
|
+
"Auth",
|
|
38
|
+
"BaseMiddleware",
|
|
39
|
+
"BearerAuth",
|
|
40
|
+
"ClientConfig",
|
|
41
|
+
"CyclePolicy",
|
|
42
|
+
"DiagnosticRenderError",
|
|
43
|
+
"DiagnosticSnapshot",
|
|
44
|
+
"Field",
|
|
45
|
+
"GraphQLClient",
|
|
46
|
+
"GraphQLFieldError",
|
|
47
|
+
"GraphQLResponse",
|
|
48
|
+
"HeaderAuth",
|
|
49
|
+
"Middleware",
|
|
50
|
+
"Node",
|
|
51
|
+
"NodeList",
|
|
52
|
+
"RequestInfo",
|
|
53
|
+
"ResponseShapeError",
|
|
54
|
+
"SchemaSource",
|
|
55
|
+
"Selection",
|
|
56
|
+
"SelectionPolicy",
|
|
57
|
+
"Transport",
|
|
58
|
+
"__version__",
|
|
59
|
+
"build_client",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
__version__ = "0.1.0a1"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The ``Auth`` protocol and the two implementations that ship (B5).
|
|
2
|
+
|
|
3
|
+
``Auth.apply`` is called once per request, so a token that has to be refreshed
|
|
4
|
+
is refreshed inside ``apply`` and needs no extra machinery. It sits fourth in
|
|
5
|
+
the C4 header precedence chain: above ``ClientConfig.headers`` and the
|
|
6
|
+
``gql_headers`` fixture, below ``with_headers()`` and a per-call ``headers=``.
|
|
7
|
+
|
|
8
|
+
``apply`` receives the live ``RequestInfo`` (C2), because an implementation
|
|
9
|
+
genuinely needs the real values to sign or refresh a request. The leak
|
|
10
|
+
boundary is rendering, not the object, so nothing here may render one: this
|
|
11
|
+
module produces a new ``RequestInfo`` and never text.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import dataclasses
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
from typing import Protocol, runtime_checkable
|
|
19
|
+
|
|
20
|
+
from pytest_graphql._core.diagnostics import RequestInfo
|
|
21
|
+
from pytest_graphql._core.headers import merge_headers
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class Auth(Protocol):
|
|
26
|
+
"""One request identity (B5)."""
|
|
27
|
+
|
|
28
|
+
def apply(self, request: RequestInfo) -> RequestInfo: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def with_headers(request: RequestInfo, headers: Mapping[str, str]) -> RequestInfo:
|
|
32
|
+
"""``request`` with ``headers`` merged over its own, under the C4 rule.
|
|
33
|
+
|
|
34
|
+
The one place an ``Auth`` implementation in this package rewrites a
|
|
35
|
+
request, so the case-insensitive replacement rule is applied once rather
|
|
36
|
+
than at each implementation.
|
|
37
|
+
"""
|
|
38
|
+
return dataclasses.replace(request, headers=merge_headers(request.headers, headers))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BearerAuth:
|
|
42
|
+
"""``Authorization: Bearer <token>`` (B5).
|
|
43
|
+
|
|
44
|
+
The token is held as given. It reaches no representation: ``RequestInfo``
|
|
45
|
+
renders only its redacted snapshot, and ``authorization`` is in the
|
|
46
|
+
default ``redact_headers`` set, so the rendered form is a placeholder and
|
|
47
|
+
the value enters the C16 secret set for the free-form-text scrub.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
__slots__ = ("token",)
|
|
51
|
+
|
|
52
|
+
def __init__(self, token: str) -> None:
|
|
53
|
+
self.token = token
|
|
54
|
+
|
|
55
|
+
def apply(self, request: RequestInfo) -> RequestInfo:
|
|
56
|
+
return with_headers(request, {"Authorization": f"Bearer {self.token}"})
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return "BearerAuth(token=<redacted>)"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class HeaderAuth:
|
|
63
|
+
"""A fixed set of identity headers (B5).
|
|
64
|
+
|
|
65
|
+
B5 writes the constructor as ``HeaderAuth(**headers)``. A keyword cannot
|
|
66
|
+
spell a name containing a hyphen, and almost every real identity header
|
|
67
|
+
does, so an optional positional mapping is accepted as well, exactly as
|
|
68
|
+
B21 does for ``with_headers()`` and for the same reason:
|
|
69
|
+
``HeaderAuth({"X-Api-Key": "v"}, Authorization="Bearer x")``.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
__slots__ = ("headers",)
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self, headers: Mapping[str, str] | None = None, /, **named: str
|
|
76
|
+
) -> None:
|
|
77
|
+
self.headers = merge_headers(headers, named)
|
|
78
|
+
|
|
79
|
+
def apply(self, request: RequestInfo) -> RequestInfo:
|
|
80
|
+
return with_headers(request, self.headers)
|
|
81
|
+
|
|
82
|
+
def __repr__(self) -> str:
|
|
83
|
+
names = ", ".join(sorted(self.headers))
|
|
84
|
+
return f"HeaderAuth(names=[{names}], values=<redacted>)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def as_(auth: Auth | str) -> Auth:
|
|
88
|
+
"""Resolve the documented shorthand: a bare string is a bearer token."""
|
|
89
|
+
return BearerAuth(auth) if isinstance(auth, str) else auth
|