schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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.
Files changed (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,100 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from ..exceptions import CheckFailed, get_grouped_exception
6
- from ..internal.checks import CheckContext
7
-
8
- if TYPE_CHECKING:
9
- from ..failures import FailureContext
10
- from ..internal.checks import CheckFunction
11
- from ..models import Case
12
- from ..transports.responses import GenericResponse
13
- from .context import RunnerContext
14
-
15
-
16
- def validate_response(
17
- *,
18
- response: GenericResponse,
19
- case: Case,
20
- runner_ctx: RunnerContext,
21
- check_ctx: CheckContext,
22
- checks: tuple[CheckFunction, ...],
23
- additional_checks: tuple[CheckFunction, ...] = (),
24
- max_response_time: int | None = None,
25
- ) -> None:
26
- """Validate the response against the provided checks."""
27
- from .._compat import MultipleFailures
28
- from ..checks import _make_max_response_time_failure_message
29
- from ..failures import ResponseTimeExceeded
30
- from ..models import Check, Status
31
-
32
- exceptions: list[CheckFailed | AssertionError] = []
33
- check_results = runner_ctx.checks_for_step
34
-
35
- def _on_failure(exc: CheckFailed | AssertionError, message: str, context: FailureContext | None) -> None:
36
- exceptions.append(exc)
37
- if runner_ctx.is_seen_in_suite(exc):
38
- return
39
- failed_check = Check(
40
- name=name,
41
- value=Status.failure,
42
- response=response,
43
- elapsed=response.elapsed.total_seconds(),
44
- example=copied_case,
45
- message=message,
46
- context=context,
47
- request=None,
48
- )
49
- runner_ctx.add_failed_check(failed_check)
50
- check_results.append(failed_check)
51
- runner_ctx.mark_as_seen_in_suite(exc)
52
-
53
- def _on_passed(_name: str, _case: Case) -> None:
54
- passed_check = Check(
55
- name=_name,
56
- value=Status.success,
57
- response=response,
58
- elapsed=response.elapsed.total_seconds(),
59
- example=_case,
60
- request=None,
61
- )
62
- check_results.append(passed_check)
63
-
64
- for check in tuple(checks) + tuple(additional_checks):
65
- name = check.__name__
66
- copied_case = case.partial_deepcopy()
67
- try:
68
- skip_check = check(check_ctx, response, copied_case)
69
- if not skip_check:
70
- _on_passed(name, copied_case)
71
- except CheckFailed as exc:
72
- if runner_ctx.is_seen_in_run(exc):
73
- continue
74
- _on_failure(exc, str(exc), exc.context)
75
- except AssertionError as exc:
76
- if runner_ctx.is_seen_in_run(exc):
77
- continue
78
- _on_failure(exc, str(exc) or f"Custom check failed: `{name}`", None)
79
- except MultipleFailures as exc:
80
- for subexc in exc.exceptions:
81
- if runner_ctx.is_seen_in_run(subexc):
82
- continue
83
- _on_failure(subexc, str(subexc), subexc.context)
84
-
85
- if max_response_time:
86
- elapsed_time = response.elapsed.total_seconds() * 1000
87
- if elapsed_time > max_response_time:
88
- message = _make_max_response_time_failure_message(elapsed_time, max_response_time)
89
- context = ResponseTimeExceeded(message=message, elapsed=elapsed_time, deadline=max_response_time)
90
- try:
91
- raise AssertionError(message)
92
- except AssertionError as _exc:
93
- if not runner_ctx.is_seen_in_run(_exc):
94
- _on_failure(_exc, message, context)
95
- else:
96
- _on_passed("max_response_time", case)
97
-
98
- # Raise a grouped exception so Hypothesis can properly deduplicate it against the other failures
99
- if exceptions:
100
- raise get_grouped_exception(case.operation.verbose_name, *exceptions)(causes=tuple(exceptions))
schemathesis/targets.py DELETED
@@ -1,77 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Callable
5
-
6
- if TYPE_CHECKING:
7
- from .models import Case
8
- from .transports.responses import GenericResponse
9
-
10
-
11
- @dataclass
12
- class TargetContext:
13
- """Context for targeted testing.
14
-
15
- :ivar Case case: Generated example that is being processed.
16
- :ivar GenericResponse response: API response.
17
- :ivar float response_time: API response time.
18
- """
19
-
20
- case: Case
21
- response: GenericResponse
22
- response_time: float
23
-
24
-
25
- def response_time(context: TargetContext) -> float:
26
- return context.response_time
27
-
28
-
29
- Target = Callable[[TargetContext], float]
30
- DEFAULT_TARGETS = ()
31
- OPTIONAL_TARGETS = (response_time,)
32
- ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
33
-
34
-
35
- @dataclass
36
- class TargetMetricCollector:
37
- """Collect multiple observations for target metrics."""
38
-
39
- targets: list[Target]
40
- observations: dict[str, list[int | float]] = field(init=False)
41
-
42
- def __post_init__(self) -> None:
43
- self.observations = {target.__name__: [] for target in self.targets}
44
-
45
- def reset(self) -> None:
46
- """Reset all collected observations."""
47
- for target in self.targets:
48
- self.observations[target.__name__].clear()
49
-
50
- def store(self, case: Case, response: GenericResponse) -> None:
51
- """Calculate target metrics & store them."""
52
- context = TargetContext(case=case, response=response, response_time=response.elapsed.total_seconds())
53
- for target in self.targets:
54
- self.observations[target.__name__].append(target(context))
55
-
56
- def maximize(self) -> None:
57
- """Give feedback to the Hypothesis engine, so it maximizes the aggregated metrics."""
58
- import hypothesis
59
-
60
- for target in self.targets:
61
- # Currently aggregation is just a sum
62
- metric = sum(self.observations[target.__name__])
63
- hypothesis.target(metric, label=target.__name__)
64
-
65
-
66
- def register(target: Target) -> Target:
67
- """Register a new testing target for schemathesis CLI.
68
-
69
- :param target: A function that will be called to calculate a metric passed to ``hypothesis.target``.
70
- """
71
- from . import cli
72
-
73
- global ALL_TARGETS
74
-
75
- ALL_TARGETS += (target,)
76
- cli.TARGETS_TYPE.choices += (target.__name__,) # type: ignore
77
- return target
@@ -1,359 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import inspect
5
- import time
6
- from contextlib import contextmanager
7
- from dataclasses import dataclass
8
- from datetime import timedelta
9
- from inspect import iscoroutinefunction
10
- from typing import TYPE_CHECKING, Any, Generator, Protocol, TypeVar, cast
11
- from urllib.parse import urlparse
12
-
13
- from .. import failures
14
- from .._dependency_versions import IS_WERKZEUG_ABOVE_3
15
- from ..constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET
16
- from ..exceptions import get_timeout_error
17
- from ..serializers import SerializerContext
18
- from ..types import Cookies, NotSet, RequestCert
19
-
20
- if TYPE_CHECKING:
21
- import requests
22
- import werkzeug
23
- from _typeshed.wsgi import WSGIApplication
24
- from starlette_testclient._testclient import ASGI2App, ASGI3App
25
-
26
- from ..models import Case
27
- from .responses import WSGIResponse
28
-
29
-
30
- @dataclass
31
- class RequestConfig:
32
- timeout: int | None = None
33
- tls_verify: bool | str = True
34
- proxy: str | None = None
35
- cert: RequestCert | None = None
36
-
37
- def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
38
-
39
- @property
40
- def prepared_timeout(self) -> float | None:
41
- return prepare_timeout(self.timeout)
42
-
43
-
44
- def serialize_payload(payload: bytes) -> str:
45
- return base64.b64encode(payload).decode()
46
-
47
-
48
- def deserialize_payload(data: str | None) -> bytes | None:
49
- if data is None:
50
- return None
51
- return base64.b64decode(data)
52
-
53
-
54
- def get(app: Any) -> Transport:
55
- """Get transport to send the data to the application."""
56
- if app is None:
57
- return RequestsTransport()
58
- if iscoroutinefunction(app) or (
59
- hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
60
- ):
61
- return ASGITransport(app=app)
62
- return WSGITransport(app=app)
63
-
64
-
65
- S = TypeVar("S", contravariant=True)
66
- R = TypeVar("R", covariant=True)
67
-
68
-
69
- class Transport(Protocol[S, R]):
70
- def serialize_case(
71
- self,
72
- case: Case,
73
- *,
74
- base_url: str | None = None,
75
- headers: dict[str, Any] | None = None,
76
- params: dict[str, Any] | None = None,
77
- cookies: dict[str, Any] | None = None,
78
- ) -> dict[str, Any]:
79
- raise NotImplementedError
80
-
81
- def send(
82
- self,
83
- case: Case,
84
- *,
85
- session: S | None = None,
86
- base_url: str | None = None,
87
- headers: dict[str, Any] | None = None,
88
- params: dict[str, Any] | None = None,
89
- cookies: dict[str, Any] | None = None,
90
- **kwargs: Any,
91
- ) -> R:
92
- raise NotImplementedError
93
-
94
-
95
- class RequestsTransport:
96
- def serialize_case(
97
- self,
98
- case: Case,
99
- *,
100
- base_url: str | None = None,
101
- headers: dict[str, Any] | None = None,
102
- params: dict[str, Any] | None = None,
103
- cookies: dict[str, Any] | None = None,
104
- ) -> dict[str, Any]:
105
- final_headers = case._get_headers(headers)
106
- media_type: str | None
107
- if case.body is not NOT_SET and case.media_type is None:
108
- media_type = case.operation._get_default_media_type()
109
- else:
110
- media_type = case.media_type
111
- if media_type and media_type != "multipart/form-data" and not isinstance(case.body, NotSet):
112
- # `requests` will handle multipart form headers with the proper `boundary` value.
113
- if "content-type" not in final_headers:
114
- final_headers["Content-Type"] = media_type
115
- url = case._get_url(base_url)
116
- serializer = case._get_serializer(media_type)
117
- if serializer is not None and not isinstance(case.body, NotSet):
118
- context = SerializerContext(case=case)
119
- extra = serializer.as_requests(context, case._get_body())
120
- else:
121
- extra = {}
122
- if case._auth is not None:
123
- extra["auth"] = case._auth
124
- additional_headers = extra.pop("headers", None)
125
- if additional_headers:
126
- # Additional headers, needed for the serializer
127
- for key, value in additional_headers.items():
128
- final_headers.setdefault(key, value)
129
- data = {
130
- "method": case.method,
131
- "url": url,
132
- "cookies": case.cookies,
133
- "headers": final_headers,
134
- "params": case.query,
135
- **extra,
136
- }
137
- if params is not None:
138
- _merge_dict_to(data, "params", params)
139
- if cookies is not None:
140
- _merge_dict_to(data, "cookies", cookies)
141
- return data
142
-
143
- def send(
144
- self,
145
- case: Case,
146
- *,
147
- session: requests.Session | None = None,
148
- base_url: str | None = None,
149
- headers: dict[str, Any] | None = None,
150
- params: dict[str, Any] | None = None,
151
- cookies: dict[str, Any] | None = None,
152
- **kwargs: Any,
153
- ) -> requests.Response:
154
- import requests
155
- from urllib3.exceptions import ReadTimeoutError
156
-
157
- data = self.serialize_case(case, base_url=base_url, headers=headers, params=params, cookies=cookies)
158
- data.update(kwargs)
159
- data.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
160
- if session is None:
161
- validate_vanilla_requests_kwargs(data)
162
- session = requests.Session()
163
- close_session = True
164
- else:
165
- close_session = False
166
- verify = data.get("verify", True)
167
- try:
168
- with case.operation.schema.ratelimit():
169
- response = session.request(**data) # type: ignore
170
- except (requests.Timeout, requests.ConnectionError) as exc:
171
- if isinstance(exc, requests.ConnectionError):
172
- if not isinstance(exc.args[0], ReadTimeoutError):
173
- raise
174
- req = requests.Request(
175
- method=data["method"].upper(),
176
- url=data["url"],
177
- headers=data["headers"],
178
- files=data.get("files"),
179
- data=data.get("data") or {},
180
- json=data.get("json"),
181
- params=data.get("params") or {},
182
- auth=data.get("auth"),
183
- cookies=data["cookies"],
184
- hooks=data.get("hooks"),
185
- )
186
- request = session.prepare_request(req)
187
- else:
188
- request = cast(requests.PreparedRequest, exc.request)
189
- timeout = 1000 * data["timeout"] # It is defined and not empty, since the exception happened
190
- code_message = case._get_code_message(case.operation.schema.code_sample_style, request, verify=verify)
191
- message = f"The server failed to respond within the specified limit of {timeout:.2f}ms"
192
- raise get_timeout_error(case.operation.verbose_name, timeout)(
193
- f"\n\n1. {failures.RequestTimeout.title}\n\n{message}\n\n{code_message}",
194
- context=failures.RequestTimeout(message=message, timeout=timeout),
195
- ) from None
196
- response.verify = verify # type: ignore[attr-defined]
197
- response._session = session # type: ignore[attr-defined]
198
- if close_session:
199
- session.close()
200
- return response
201
-
202
-
203
- def _merge_dict_to(data: dict[str, Any], data_key: str, new: dict[str, Any]) -> None:
204
- original = data[data_key] or {}
205
- for key, value in new.items():
206
- original[key] = value
207
- data[data_key] = original
208
-
209
-
210
- def prepare_timeout(timeout: int | None) -> float | None:
211
- """Request timeout is in milliseconds, but `requests` uses seconds."""
212
- output: int | float | None = timeout
213
- if timeout is not None:
214
- output = timeout / 1000
215
- return output
216
-
217
-
218
- def validate_vanilla_requests_kwargs(data: dict[str, Any]) -> None:
219
- """Check arguments for `requests.Session.request`.
220
-
221
- Some arguments can be valid for cases like ASGI integration, but at the same time they won't work for the regular
222
- `requests` calls. In such cases we need to avoid an obscure error message, that comes from `requests`.
223
- """
224
- url = data["url"]
225
- if not urlparse(url).netloc:
226
- stack = inspect.stack()
227
- method_name = "call"
228
- for frame in stack[1:]:
229
- if frame.function == "call_and_validate":
230
- method_name = "call_and_validate"
231
- break
232
- raise RuntimeError(
233
- "The `base_url` argument is required when specifying a schema via a file, so Schemathesis knows where to send the data. \n"
234
- f"Pass `base_url` either to the `schemathesis.from_*` loader or to the `Case.{method_name}`.\n"
235
- f"If you use the ASGI integration, please supply your test client "
236
- f"as the `session` argument to `call`.\nURL: {url}"
237
- )
238
-
239
-
240
- @dataclass
241
- class ASGITransport(RequestsTransport):
242
- app: ASGI2App | ASGI3App
243
-
244
- def send(
245
- self,
246
- case: Case,
247
- *,
248
- session: requests.Session | None = None,
249
- base_url: str | None = None,
250
- headers: dict[str, Any] | None = None,
251
- params: dict[str, Any] | None = None,
252
- cookies: dict[str, Any] | None = None,
253
- **kwargs: Any,
254
- ) -> requests.Response:
255
- from starlette_testclient import TestClient as ASGIClient
256
-
257
- if base_url is None:
258
- base_url = case.get_full_base_url()
259
- with ASGIClient(self.app) as client:
260
- return super().send(
261
- case, session=client, base_url=base_url, headers=headers, params=params, cookies=cookies, **kwargs
262
- )
263
-
264
-
265
- @dataclass
266
- class WSGITransport:
267
- app: WSGIApplication
268
-
269
- def serialize_case(
270
- self,
271
- case: Case,
272
- *,
273
- base_url: str | None = None,
274
- headers: dict[str, Any] | None = None,
275
- params: dict[str, Any] | None = None,
276
- cookies: dict[str, Any] | None = None,
277
- ) -> dict[str, Any]:
278
- final_headers = case._get_headers(headers)
279
- media_type: str | None
280
- if case.body is not NOT_SET and case.media_type is None:
281
- media_type = case.operation._get_default_media_type()
282
- else:
283
- media_type = case.media_type
284
- if media_type and not isinstance(case.body, NotSet):
285
- # If we need to send a payload, then the Content-Type header should be set
286
- final_headers["Content-Type"] = media_type
287
- extra: dict[str, Any]
288
- serializer = case._get_serializer(media_type)
289
- if serializer is not None and not isinstance(case.body, NotSet):
290
- context = SerializerContext(case=case)
291
- extra = serializer.as_werkzeug(context, case._get_body())
292
- else:
293
- extra = {}
294
- data = {
295
- "method": case.method,
296
- "path": case.operation.schema.get_full_path(case.formatted_path),
297
- # Convert to a regular dictionary, as we use `CaseInsensitiveDict` which is not supported by Werkzeug
298
- "headers": dict(final_headers),
299
- "query_string": case.query,
300
- **extra,
301
- }
302
- if params is not None:
303
- _merge_dict_to(data, "query_string", params)
304
- return data
305
-
306
- def send(
307
- self,
308
- case: Case,
309
- *,
310
- session: Any = None,
311
- base_url: str | None = None,
312
- headers: dict[str, Any] | None = None,
313
- params: dict[str, Any] | None = None,
314
- cookies: dict[str, Any] | None = None,
315
- **kwargs: Any,
316
- ) -> WSGIResponse:
317
- import requests
318
- import werkzeug
319
-
320
- from .responses import WSGIResponse
321
-
322
- application = kwargs.pop("app", self.app) or self.app
323
- data = self.serialize_case(case, headers=headers, params=params)
324
- data.update(kwargs)
325
- client = werkzeug.Client(application, WSGIResponse)
326
- cookies = {**(case.cookies or {}), **(cookies or {})}
327
- with cookie_handler(client, cookies), case.operation.schema.ratelimit():
328
- start = time.monotonic()
329
- response = client.open(**data)
330
- elapsed = time.monotonic() - start
331
- requests_kwargs = RequestsTransport().serialize_case(
332
- case,
333
- base_url=case.get_full_base_url(),
334
- headers=headers,
335
- params=params,
336
- cookies=cookies,
337
- )
338
- response.request = requests.Request(**requests_kwargs).prepare()
339
- response.elapsed = timedelta(seconds=elapsed)
340
- return response
341
-
342
-
343
- @contextmanager
344
- def cookie_handler(client: werkzeug.Client, cookies: Cookies | None) -> Generator[None, None, None]:
345
- """Set cookies required for a call."""
346
- if not cookies:
347
- yield
348
- else:
349
- for key, value in cookies.items():
350
- if IS_WERKZEUG_ABOVE_3:
351
- client.set_cookie(key=key, value=value, domain="localhost")
352
- else:
353
- client.set_cookie("localhost", key=key, value=value)
354
- yield
355
- for key in cookies:
356
- if IS_WERKZEUG_ABOVE_3:
357
- client.delete_cookie(key=key, domain="localhost")
358
- else:
359
- client.delete_cookie("localhost", key=key)
@@ -1,7 +0,0 @@
1
- from inspect import iscoroutinefunction
2
-
3
-
4
- def is_asgi_app(app: object) -> bool:
5
- return iscoroutinefunction(app) or (
6
- hasattr(app, "__call__") and iscoroutinefunction(app.__call__) # noqa: B004
7
- )
@@ -1,38 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any
4
-
5
- from ..constants import USER_AGENT
6
-
7
- if TYPE_CHECKING:
8
- from requests.auth import HTTPDigestAuth
9
-
10
- from ..types import RawAuth
11
-
12
-
13
- def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
14
- from requests.auth import HTTPDigestAuth
15
-
16
- if auth and auth_type == "digest":
17
- return HTTPDigestAuth(*auth)
18
- return auth
19
-
20
-
21
- def prepare_wsgi_headers(headers: dict[str, Any] | None, auth: RawAuth | None, auth_type: str | None) -> dict[str, Any]:
22
- headers = headers or {}
23
- if "user-agent" not in {header.lower() for header in headers}:
24
- headers["User-Agent"] = USER_AGENT
25
- wsgi_auth = get_wsgi_auth(auth, auth_type)
26
- if wsgi_auth:
27
- headers["Authorization"] = wsgi_auth
28
- return headers
29
-
30
-
31
- def get_wsgi_auth(auth: RawAuth | None, auth_type: str | None) -> str | None:
32
- from requests.auth import _basic_auth_str
33
-
34
- if auth:
35
- if auth_type == "digest":
36
- raise ValueError("Digest auth is not supported for WSGI apps")
37
- return _basic_auth_str(*auth)
38
- return None
@@ -1,36 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import Any
5
-
6
- from ..constants import USER_AGENT
7
-
8
-
9
- def setup_default_headers(kwargs: dict[str, Any]) -> None:
10
- headers = kwargs.setdefault("headers", {})
11
- if "user-agent" not in {header.lower() for header in headers}:
12
- kwargs["headers"]["User-Agent"] = USER_AGENT
13
-
14
-
15
- def is_latin_1_encodable(value: str) -> bool:
16
- """Header values are encoded to latin-1 before sending."""
17
- try:
18
- value.encode("latin-1")
19
- return True
20
- except UnicodeEncodeError:
21
- return False
22
-
23
-
24
- # Adapted from http.client._is_illegal_header_value
25
- INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
26
-
27
-
28
- def has_invalid_characters(name: str, value: str) -> bool:
29
- from requests.exceptions import InvalidHeader
30
- from requests.utils import check_header_validity
31
-
32
- try:
33
- check_header_validity((name, value))
34
- return bool(INVALID_HEADER_RE.search(value))
35
- except InvalidHeader:
36
- return True
@@ -1,57 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import sys
5
- from typing import TYPE_CHECKING, Any, NoReturn, Union
6
-
7
- from werkzeug.wrappers import Response as BaseResponse
8
-
9
- from .._compat import JSONMixin
10
-
11
- if TYPE_CHECKING:
12
- from datetime import timedelta
13
-
14
- from httpx import Response as httpxResponse
15
- from requests import PreparedRequest
16
- from requests import Response as requestsResponse
17
-
18
-
19
- class WSGIResponse(BaseResponse, JSONMixin):
20
- # We store "requests" request to build a reproduction code
21
- request: PreparedRequest
22
- elapsed: timedelta
23
-
24
- def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
25
- # We don't need a werkzeug-specific exception when JSON parsing error happens
26
- raise e
27
-
28
-
29
- def get_payload(response: GenericResponse) -> str:
30
- from httpx import Response as httpxResponse
31
- from requests import Response as requestsResponse
32
-
33
- if isinstance(response, (httpxResponse, requestsResponse)):
34
- return response.text
35
- return response.get_data(as_text=True)
36
-
37
-
38
- def get_json(response: GenericResponse) -> Any:
39
- from httpx import Response as httpxResponse
40
- from requests import Response as requestsResponse
41
-
42
- if isinstance(response, (httpxResponse, requestsResponse)):
43
- return json.loads(response.text)
44
- return response.json
45
-
46
-
47
- def get_reason(status_code: int) -> str:
48
- if sys.version_info < (3, 9) and status_code == 418:
49
- # Python 3.8 does not have 418 status in the `HTTPStatus` enum
50
- return "I'm a Teapot"
51
-
52
- import http.client
53
-
54
- return http.client.responses.get(status_code, "Unknown")
55
-
56
-
57
- GenericResponse = Union["httpxResponse", "requestsResponse", WSGIResponse]