schemathesis 3.25.6__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +783 -432
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +22 -5
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +258 -112
  23. schemathesis/cli/output/short.py +23 -8
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +318 -211
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +50 -15
  63. schemathesis/runner/events.py +65 -5
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +388 -177
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/runner/probes.py +11 -9
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +7 -2
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +45 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +78 -60
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +126 -12
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +360 -241
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.6.dist-info/METADATA +0 -356
  144. schemathesis-3.25.6.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.6.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
@@ -2,39 +2,46 @@
2
2
 
3
3
  They all consist of primitive types and don't have references to schemas, app, etc.
4
4
  """
5
+
5
6
  from __future__ import annotations
7
+
6
8
  import logging
7
9
  import re
8
- from dataclasses import dataclass, field
9
- from typing import Any, TYPE_CHECKING, cast
10
+ import textwrap
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import TYPE_CHECKING, Any, cast
10
13
 
11
- from ..transports import serialize_payload
12
14
  from ..code_samples import get_excluded_headers
13
15
  from ..exceptions import (
14
- FailureContext,
15
- InternalError,
16
- make_unique_by_key,
17
- format_exception,
18
- extract_requests_exception_details,
19
- RuntimeErrorType,
20
- DeadlineExceeded,
21
- OperationSchemaError,
22
16
  BodyInGetRequestError,
17
+ DeadlineExceeded,
18
+ InternalError,
23
19
  InvalidRegularExpression,
20
+ OperationSchemaError,
21
+ RecursiveReferenceError,
22
+ RuntimeErrorType,
24
23
  SerializationError,
25
24
  UnboundPrefixError,
25
+ extract_requests_exception_details,
26
+ format_exception,
27
+ make_unique_by_key,
26
28
  )
27
- from ..models import Case, Check, Interaction, Request, Response, Status, TestResult
29
+ from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
30
+ from ..transports import deserialize_payload, serialize_payload
28
31
 
29
32
  if TYPE_CHECKING:
30
33
  import hypothesis.errors
31
34
  from requests.structures import CaseInsensitiveDict
32
35
 
36
+ from ..failures import FailureContext
37
+ from ..generation import DataGenerationMethod
38
+
33
39
 
34
40
  @dataclass
35
41
  class SerializedCase:
36
42
  # Case data
37
43
  id: str
44
+ generation_time: float
38
45
  path_parameters: dict[str, Any] | None
39
46
  headers: dict[str, Any] | None
40
47
  cookies: dict[str, Any] | None
@@ -46,7 +53,9 @@ class SerializedCase:
46
53
  method: str
47
54
  url: str
48
55
  path_template: str
56
+ full_path: str
49
57
  verbose_name: str
58
+ transition_id: TransitionId | None
50
59
  # Transport info
51
60
  verify: bool
52
61
  # Headers coming from sources outside data generation
@@ -59,6 +68,7 @@ class SerializedCase:
59
68
  serialized_body = _serialize_body(request_data.body)
60
69
  return cls(
61
70
  id=case.id,
71
+ generation_time=case.generation_time,
62
72
  path_parameters=case.path_parameters,
63
73
  headers=dict(case.headers) if case.headers is not None else None,
64
74
  cookies=case.cookies,
@@ -71,11 +81,21 @@ class SerializedCase:
71
81
  method=case.method,
72
82
  url=request_data.url,
73
83
  path_template=case.path,
84
+ full_path=case.full_path,
74
85
  verbose_name=case.operation.verbose_name,
86
+ transition_id=case.source.transition_id if case.source is not None else None,
75
87
  verify=verify,
76
88
  extra_headers=request_data.headers,
77
89
  )
78
90
 
91
+ def deserialize_body(self) -> bytes | None:
92
+ """Deserialize the test case body.
93
+
94
+ `SerializedCase` should be serializable to JSON, therefore body is encoded as base64 string
95
+ to support arbitrary binary data.
96
+ """
97
+ return deserialize_payload(self.body)
98
+
79
99
 
80
100
  def _serialize_body(body: str | bytes | None) -> str | None:
81
101
  if body is None:
@@ -105,6 +125,7 @@ class SerializedCheck:
105
125
  @classmethod
106
126
  def from_check(cls, check: Check) -> SerializedCheck:
107
127
  import requests
128
+
108
129
  from ..transports.responses import WSGIResponse
109
130
 
110
131
  if check.response is not None:
@@ -137,9 +158,32 @@ class SerializedCheck:
137
158
  history=history,
138
159
  )
139
160
 
161
+ @property
162
+ def title(self) -> str:
163
+ if self.context is not None:
164
+ return self.context.title
165
+ return f"Custom check failed: `{self.name}`"
166
+
167
+ @property
168
+ def formatted_message(self) -> str | None:
169
+ if self.context is not None:
170
+ if self.context.message:
171
+ message = self.context.message
172
+ else:
173
+ message = None
174
+ else:
175
+ message = self.message
176
+ if message is not None:
177
+ message = textwrap.indent(message, prefix=" ")
178
+ return message
179
+
140
180
 
141
181
  def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
142
- return {key: value[0] for key, value in headers.items() if key not in get_excluded_headers()}
182
+ return {
183
+ key: value[0] if isinstance(value, list) else value
184
+ for key, value in headers.items()
185
+ if key not in get_excluded_headers()
186
+ }
143
187
 
144
188
 
145
189
  @dataclass
@@ -200,8 +244,8 @@ class SerializedError:
200
244
 
201
245
  @classmethod
202
246
  def from_exception(cls, exception: Exception) -> SerializedError:
203
- import requests
204
247
  import hypothesis.errors
248
+ import requests
205
249
  from hypothesis import HealthCheck
206
250
 
207
251
  title = "Runtime Error"
@@ -219,6 +263,11 @@ class SerializedError:
219
263
  type_ = RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED
220
264
  message = str(exception).strip()
221
265
  extras = []
266
+ elif isinstance(exception, RecursiveReferenceError):
267
+ type_ = RuntimeErrorType.SCHEMA_UNSUPPORTED
268
+ message = str(exception).strip()
269
+ extras = []
270
+ title = "Unsupported Schema"
222
271
  elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).startswith("Scalar "):
223
272
  # Comes from `hypothesis-graphql`
224
273
  scalar_name = _scalar_name_from_error(exception)
@@ -226,8 +275,10 @@ class SerializedError:
226
275
  message = f"Scalar type '{scalar_name}' is not recognized"
227
276
  extras = []
228
277
  title = "Unknown GraphQL Scalar"
229
- elif isinstance(exception, hypothesis.errors.InvalidArgument) and str(exception).endswith(
230
- "larger than Hypothesis is designed to handle"
278
+ elif (
279
+ isinstance(exception, hypothesis.errors.InvalidArgument)
280
+ and str(exception).endswith("larger than Hypothesis is designed to handle")
281
+ or "can never generate an example, because min_size is larger than Hypothesis supports" in str(exception)
231
282
  ):
232
283
  type_ = RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE
233
284
  message = HEALTH_CHECK_MESSAGE_LARGE_BASE_EXAMPLE
@@ -238,6 +289,7 @@ class SerializedError:
238
289
  message = f"{exception}. Possible reasons:"
239
290
  extras = [
240
291
  "- Contradictory schema constraints, such as a minimum value exceeding the maximum.",
292
+ "- Invalid schema definitions for headers or cookies, for example allowing for non-ASCII characters.",
241
293
  "- Excessive schema complexity, which hinders parameter generation.",
242
294
  ]
243
295
  title = "Schema Error"
@@ -339,9 +391,15 @@ def _scalar_name_from_error(exception: hypothesis.errors.InvalidArgument) -> str
339
391
  @dataclass
340
392
  class SerializedInteraction:
341
393
  request: Request
342
- response: Response
394
+ response: Response | None
343
395
  checks: list[SerializedCheck]
344
396
  status: Status
397
+ data_generation_method: DataGenerationMethod
398
+ phase: TestPhase | None
399
+ description: str | None
400
+ location: str | None
401
+ parameter: str | None
402
+ parameter_location: str | None
345
403
  recorded_at: str
346
404
 
347
405
  @classmethod
@@ -351,6 +409,12 @@ class SerializedInteraction:
351
409
  response=interaction.response,
352
410
  checks=[SerializedCheck.from_check(check) for check in interaction.checks],
353
411
  status=interaction.status,
412
+ data_generation_method=interaction.data_generation_method,
413
+ phase=interaction.phase,
414
+ description=interaction.description,
415
+ location=interaction.location,
416
+ parameter=interaction.parameter,
417
+ parameter_location=interaction.parameter_location,
354
418
  recorded_at=interaction.recorded_at,
355
419
  )
356
420
 
@@ -409,3 +473,72 @@ def deduplicate_failures(checks: list[SerializedCheck]) -> list[SerializedCheck]
409
473
  unique_checks.append(check)
410
474
  seen.add(key)
411
475
  return unique_checks
476
+
477
+
478
+ def _serialize_case(case: SerializedCase) -> dict[str, Any]:
479
+ return {
480
+ "id": case.id,
481
+ "generation_time": case.generation_time,
482
+ "verbose_name": case.verbose_name,
483
+ "path_template": case.path_template,
484
+ "path_parameters": stringify_path_parameters(case.path_parameters),
485
+ "query": prepare_query(case.query),
486
+ "cookies": case.cookies,
487
+ "media_type": case.media_type,
488
+ }
489
+
490
+
491
+ def _serialize_response(response: Response) -> dict[str, Any]:
492
+ return {
493
+ "status_code": response.status_code,
494
+ "headers": response.headers,
495
+ "body": response.body,
496
+ "encoding": response.encoding,
497
+ "elapsed": response.elapsed,
498
+ }
499
+
500
+
501
+ def _serialize_check(check: SerializedCheck) -> dict[str, Any]:
502
+ return {
503
+ "name": check.name,
504
+ "value": check.value,
505
+ "request": {
506
+ "method": check.request.method,
507
+ "uri": check.request.uri,
508
+ "body": check.request.body,
509
+ "headers": check.request.headers,
510
+ },
511
+ "response": _serialize_response(check.response) if check.response is not None else None,
512
+ "example": _serialize_case(check.example),
513
+ "message": check.message,
514
+ "context": asdict(check.context) if check.context is not None else None, # type: ignore
515
+ "history": [
516
+ {"case": _serialize_case(entry.case), "response": _serialize_response(entry.response)}
517
+ for entry in check.history
518
+ ],
519
+ }
520
+
521
+
522
+ def stringify_path_parameters(path_parameters: dict[str, Any] | None) -> dict[str, str]:
523
+ """Cast all path parameter values to strings.
524
+
525
+ Path parameter values may be of arbitrary type, but to display them properly they should be casted to strings.
526
+ """
527
+ return {key: str(value) for key, value in (path_parameters or {}).items()}
528
+
529
+
530
+ def prepare_query(query: dict[str, Any] | None) -> dict[str, list[str]]:
531
+ """Convert all query values to list of strings.
532
+
533
+ Query parameters may be generated in different shapes, including integers, strings, list of strings, etc.
534
+ It can also be an object, if the schema contains an object, but `style` and `explode` combo is not applicable.
535
+ """
536
+
537
+ def to_list_of_strings(value: Any) -> list[str]:
538
+ if isinstance(value, list):
539
+ return list(map(str, value))
540
+ if isinstance(value, str):
541
+ return [value]
542
+ return [str(value)]
543
+
544
+ return {key: to_list_of_strings(value) for key, value in (query or {}).items()}
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+
2
3
  import threading
3
4
  from collections.abc import MutableMapping, MutableSequence
4
5
  from dataclasses import dataclass, replace
@@ -9,6 +10,7 @@ from .constants import NOT_SET
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from requests import PreparedRequest
13
+
12
14
  from .models import Case, CaseSource, Request
13
15
  from .runner.serialization import SerializedCase, SerializedCheck, SerializedInteraction
14
16
  from .transports.responses import GenericResponse
@@ -236,6 +238,7 @@ def sanitize_serialized_check(check: SerializedCheck, *, config: Config | None =
236
238
 
237
239
 
238
240
  def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = None) -> None:
241
+ case.url = sanitize_url(case.url, config=config)
239
242
  for value in (case.path_parameters, case.headers, case.cookies, case.query, case.extra_headers):
240
243
  if value is not None:
241
244
  sanitize_value(value, config=config)
@@ -243,6 +246,7 @@ def sanitize_serialized_case(case: SerializedCase, *, config: Config | None = No
243
246
 
244
247
  def sanitize_serialized_interaction(interaction: SerializedInteraction, *, config: Config | None = None) -> None:
245
248
  sanitize_request(interaction.request, config=config)
246
- sanitize_value(interaction.response.headers, config=config)
249
+ if interaction.response is not None:
250
+ sanitize_value(interaction.response.headers, config=config)
247
251
  for check in interaction.checks:
248
252
  sanitize_serialized_check(check, config=config)