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
@@ -1,75 +1,43 @@
1
1
  from __future__ import annotations
2
- import string
3
- from base64 import b64encode
2
+
3
+ import time
4
4
  from contextlib import suppress
5
5
  from dataclasses import dataclass
6
- from functools import lru_cache
7
6
  from typing import Any, Callable, Dict, Iterable, Optional
8
7
  from urllib.parse import quote_plus
9
8
  from weakref import WeakKeyDictionary
10
9
 
11
- from hypothesis import strategies as st, reject
10
+ from hypothesis import reject
11
+ from hypothesis import strategies as st
12
12
  from hypothesis_jsonschema import from_schema
13
- from requests.auth import _basic_auth_str
14
13
  from requests.structures import CaseInsensitiveDict
15
14
  from requests.utils import to_key_val_list
16
15
 
16
+ from ... import auths, serializers
17
17
  from ..._hypothesis import prepare_urlencoded
18
18
  from ...constants import NOT_SET
19
- from .formats import STRING_FORMATS
20
- from ... import auths, serializers
19
+ from ...exceptions import BodyInGetRequestError, SerializationNotPossible
21
20
  from ...generation import DataGenerationMethod, GenerationConfig
22
- from ...internal.copy import fast_deepcopy
23
- from ...exceptions import SerializationNotPossible, BodyInGetRequestError
24
21
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
22
+ from ...internal.copy import fast_deepcopy
25
23
  from ...internal.validation import is_illegal_surrogate
26
- from ...models import APIOperation, Case, cant_serialize
24
+ from ...models import APIOperation, Case, GenerationMetadata, TestPhase, cant_serialize
27
25
  from ...transports.content_types import parse_content_type
28
26
  from ...transports.headers import has_invalid_characters, is_latin_1_encodable
29
27
  from ...types import NotSet
30
- from ...serializers import Binary
31
- from ...utils import compose, skip
28
+ from ...utils import skip
32
29
  from .constants import LOCATION_TO_CONTAINER
30
+ from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
31
+ from .media_types import MEDIA_TYPES
33
32
  from .negative import negative_schema
34
33
  from .negative.utils import can_negate
35
- from .parameters import OpenAPIBody, parameters_to_json_schema
34
+ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
36
35
  from .utils import is_header_location
37
36
 
38
- HEADER_FORMAT = "_header_value"
39
37
  SLASH = "/"
40
38
  StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
41
39
 
42
40
 
43
- def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
44
- return st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters="\n\r"))
45
-
46
-
47
- @lru_cache
48
- def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
49
- """Get all default "format" strategies."""
50
-
51
- def make_basic_auth_str(item: tuple[str, str]) -> str:
52
- return _basic_auth_str(*item)
53
-
54
- latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
55
-
56
- # Define valid characters here to avoid filtering them out in `is_valid_header` later
57
- header_value = header_values()
58
-
59
- return {
60
- "binary": st.binary().map(Binary),
61
- "byte": st.binary().map(lambda x: b64encode(x).decode()),
62
- # RFC 7230, Section 3.2.6
63
- "_header_name": st.text(
64
- min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
65
- ),
66
- # Header values with leading non-visible chars can't be sent with `requests`
67
- HEADER_FORMAT: header_value.map(str.lstrip),
68
- "_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
69
- "_bearer_auth": header_value.map("Bearer {}".format),
70
- }
71
-
72
-
73
41
  def is_valid_header(headers: dict[str, Any]) -> bool:
74
42
  """Verify if the generated headers are valid."""
75
43
  for name, value in headers.items():
@@ -117,6 +85,7 @@ def get_case_strategy(
117
85
  body: Any = NOT_SET,
118
86
  media_type: str | None = None,
119
87
  skip_on_not_negated: bool = True,
88
+ phase: TestPhase = TestPhase.GENERATE,
120
89
  ) -> Any:
121
90
  """A strategy that creates `Case` instances.
122
91
 
@@ -130,6 +99,7 @@ def get_case_strategy(
130
99
  The primary purpose of this behavior is to prevent sending incomplete explicit examples by generating missing parts
131
100
  as it works with `body`.
132
101
  """
102
+ start = time.monotonic()
133
103
  strategy_factory = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[generator]
134
104
 
135
105
  context = HookContext(operation)
@@ -175,6 +145,11 @@ def get_case_strategy(
175
145
  else:
176
146
  body_ = ValueContainer(value=body, location="body", generator=None)
177
147
  else:
148
+ # This explicit body payload comes for a media type that has a custom strategy registered
149
+ # Such strategies only support binary payloads, otherwise they can't be serialized
150
+ if not isinstance(body, bytes) and media_type in MEDIA_TYPES:
151
+ all_media_types = operation.get_request_payload_content_types()
152
+ raise SerializationNotPossible.from_media_types(*all_media_types)
178
153
  body_ = ValueContainer(value=body, location="body", generator=None)
179
154
 
180
155
  if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
@@ -187,6 +162,7 @@ def get_case_strategy(
187
162
  reject()
188
163
  instance = Case(
189
164
  operation=operation,
165
+ generation_time=time.monotonic() - start,
190
166
  media_type=media_type,
191
167
  path_parameters=path_parameters_.value,
192
168
  headers=CaseInsensitiveDict(headers_.value) if headers_.value is not None else headers_.value,
@@ -194,6 +170,18 @@ def get_case_strategy(
194
170
  query=query_.value,
195
171
  body=body_.value,
196
172
  data_generation_method=generator,
173
+ meta=GenerationMetadata(
174
+ query=query_.generator,
175
+ path_parameters=path_parameters_.generator,
176
+ headers=headers_.generator,
177
+ cookies=cookies_.generator,
178
+ body=body_.generator,
179
+ phase=phase,
180
+ description=None,
181
+ location=None,
182
+ parameter=None,
183
+ parameter_location=None,
184
+ ),
197
185
  )
198
186
  auth_context = auths.AuthContext(
199
187
  operation=operation,
@@ -212,6 +200,8 @@ def _get_body_strategy(
212
200
  operation: APIOperation,
213
201
  generation_config: GenerationConfig,
214
202
  ) -> st.SearchStrategy:
203
+ if parameter.media_type in MEDIA_TYPES:
204
+ return MEDIA_TYPES[parameter.media_type]
215
205
  # The cache key relies on object ids, which means that the parameter should not be mutated
216
206
  # Note, the parent schema is not included as each parameter belong only to one schema
217
207
  if parameter in _BODY_STRATEGIES_CACHE and strategy_factory in _BODY_STRATEGIES_CACHE[parameter]:
@@ -265,6 +255,8 @@ class ValueContainer:
265
255
  location: str
266
256
  generator: DataGenerationMethod | None
267
257
 
258
+ __slots__ = ("value", "location", "generator")
259
+
268
260
  @property
269
261
  def is_generated(self) -> bool:
270
262
  """If value was generated."""
@@ -332,6 +324,22 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
332
324
  return any(header != {"type": "string"} for header in headers.values())
333
325
 
334
326
 
327
+ def get_schema_for_location(
328
+ operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
329
+ ) -> dict[str, Any]:
330
+ schema = parameters_to_json_schema(operation, parameters)
331
+ if location == "path":
332
+ if not operation.schema.validate_schema:
333
+ # If schema validation is disabled, we try to generate data even if the parameter definition
334
+ # contains errors.
335
+ # In this case, we know that the `required` keyword should always be `True`.
336
+ schema["required"] = list(schema["properties"])
337
+ for prop in schema.get("properties", {}).values():
338
+ if prop.get("type") == "string":
339
+ prop.setdefault("minLength", 1)
340
+ return operation.schema.prepare_schema(schema)
341
+
342
+
335
343
  def get_parameters_strategy(
336
344
  operation: APIOperation,
337
345
  strategy_factory: StrategyFactory,
@@ -346,13 +354,7 @@ def get_parameters_strategy(
346
354
  nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
347
355
  if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
348
356
  return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
349
- schema = parameters_to_json_schema(operation, parameters)
350
- if not operation.schema.validate_schema and location == "path":
351
- # If schema validation is disabled, we try to generate data even if the parameter definition
352
- # contains errors.
353
- # In this case, we know that the `required` keyword should always be `True`.
354
- schema["required"] = list(schema["properties"])
355
- schema = operation.schema.prepare_schema(schema)
357
+ schema = get_schema_for_location(operation, location, parameters)
356
358
  for name in exclude:
357
359
  # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
358
360
  # that may be invalid
@@ -380,12 +382,10 @@ def get_parameters_strategy(
380
382
  # `True` / `False` / `None` improves chances of them passing validation in apps
381
383
  # that expect boolean / null types
382
384
  # and not aware of Python-specific representation of those types
383
- map_func = {
384
- "path": compose(quote_all, jsonify_python_specific_types),
385
- "query": jsonify_python_specific_types,
386
- }.get(location)
387
- if map_func:
388
- strategy = strategy.map(map_func) # type: ignore
385
+ if location == "path":
386
+ strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
387
+ elif location == "query":
388
+ strategy = strategy.map(jsonify_python_specific_types)
389
389
  _PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
390
390
  return strategy
391
391
  # No parameters defined for this location
@@ -412,6 +412,17 @@ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
412
412
  return value
413
413
 
414
414
 
415
+ def _build_custom_formats(
416
+ custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
417
+ ) -> dict[str, st.SearchStrategy]:
418
+ custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
419
+ if generation_config.headers.strategy is not None:
420
+ custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
421
+ elif not generation_config.allow_x00:
422
+ custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
423
+ return custom_formats
424
+
425
+
415
426
  def make_positive_strategy(
416
427
  schema: dict[str, Any],
417
428
  operation_name: str,
@@ -428,9 +439,10 @@ def make_positive_strategy(
428
439
  for sub_schema in schema.get("properties", {}).values():
429
440
  if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
430
441
  sub_schema.setdefault("format", HEADER_FORMAT)
442
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
431
443
  return from_schema(
432
444
  schema,
433
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
445
+ custom_formats=custom_formats,
434
446
  allow_x00=generation_config.allow_x00,
435
447
  codec=generation_config.codec,
436
448
  )
@@ -449,12 +461,13 @@ def make_negative_strategy(
449
461
  generation_config: GenerationConfig,
450
462
  custom_formats: dict[str, st.SearchStrategy] | None = None,
451
463
  ) -> st.SearchStrategy:
464
+ custom_formats = _build_custom_formats(custom_formats, generation_config)
452
465
  return negative_schema(
453
466
  schema,
454
467
  operation_name=operation_name,
455
468
  location=location,
456
469
  media_type=media_type,
457
- custom_formats={**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})},
470
+ custom_formats=custom_formats,
458
471
  generation_config=generation_config,
459
472
  )
460
473
 
@@ -478,7 +491,12 @@ def is_valid_path(parameters: dict[str, Any]) -> bool:
478
491
  disallowed_values = (SLASH, "")
479
492
 
480
493
  return not any(
481
- (value in disallowed_values or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
494
+ (
495
+ value in disallowed_values
496
+ or is_illegal_surrogate(value)
497
+ or isinstance(value, str)
498
+ and (SLASH in value or "}" in value or "{" in value)
499
+ )
482
500
  for value in parameters.values()
483
501
  )
484
502