schemathesis 3.25.5__py3-none-any.whl → 4.0.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.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,13 +1,20 @@
1
1
  from __future__ import annotations
2
+
2
3
  from itertools import chain
3
4
  from typing import Any, Callable
4
5
 
5
- from ...internal.jsonschema import traverse_schema
6
- from ...internal.copy import fast_deepcopy
6
+ from schemathesis.core.transforms import deepclone, transform
7
+
8
+ from .patterns import update_quantifier
7
9
 
8
10
 
9
11
  def to_json_schema(
10
- schema: dict[str, Any], *, nullable_name: str, copy: bool = True, is_response_schema: bool = False
12
+ schema: dict[str, Any],
13
+ *,
14
+ nullable_name: str,
15
+ copy: bool = True,
16
+ is_response_schema: bool = False,
17
+ update_quantifiers: bool = True,
11
18
  ) -> dict[str, Any]:
12
19
  """Convert Open API parameters to JSON Schema.
13
20
 
@@ -15,7 +22,7 @@ def to_json_schema(
15
22
  See a recursive version below.
16
23
  """
17
24
  if copy:
18
- schema = fast_deepcopy(schema)
25
+ schema = deepclone(schema)
19
26
  if schema.get(nullable_name) is True:
20
27
  del schema[nullable_name]
21
28
  schema = {"anyOf": [schema, {"type": "null"}]}
@@ -23,6 +30,8 @@ def to_json_schema(
23
30
  if schema_type == "file":
24
31
  schema["type"] = "string"
25
32
  schema["format"] = "binary"
33
+ if update_quantifiers:
34
+ update_pattern_in_schema(schema)
26
35
  if schema_type == "object":
27
36
  if is_response_schema:
28
37
  # Write-only properties should not occur in responses
@@ -33,6 +42,18 @@ def to_json_schema(
33
42
  return schema
34
43
 
35
44
 
45
+ def update_pattern_in_schema(schema: dict[str, Any]) -> None:
46
+ pattern = schema.get("pattern")
47
+ min_length = schema.get("minLength")
48
+ max_length = schema.get("maxLength")
49
+ if pattern and (min_length or max_length):
50
+ new_pattern = update_quantifier(pattern, min_length, max_length)
51
+ if new_pattern != pattern:
52
+ schema.pop("minLength", None)
53
+ schema.pop("maxLength", None)
54
+ schema["pattern"] = new_pattern
55
+
56
+
36
57
  def rewrite_properties(schema: dict[str, Any], predicate: Callable[[dict[str, Any]], bool]) -> None:
37
58
  required = schema.get("required", [])
38
59
  forbidden = []
@@ -71,6 +92,12 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
71
92
 
72
93
 
73
94
  def to_json_schema_recursive(
74
- schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False
95
+ schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
75
96
  ) -> dict[str, Any]:
76
- return traverse_schema(schema, to_json_schema, nullable_name=nullable_name, is_response_schema=is_response_schema)
97
+ return transform(
98
+ schema,
99
+ to_json_schema,
100
+ nullable_name=nullable_name,
101
+ is_response_schema=is_response_schema,
102
+ update_quantifiers=update_quantifiers,
103
+ )
@@ -1,8 +1,9 @@
1
1
  # These schemas are copied from https://github.com/OAI/OpenAPI-Specification/tree/master/schemas
2
2
  from __future__ import annotations
3
- from typing import Any, TYPE_CHECKING
4
3
 
5
- from ..._lazy_import import lazy_import
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from schemathesis.core.lazy_import import lazy_import
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from jsonschema import Validator
@@ -1329,6 +1330,8 @@ OPENAPI_30 = {
1329
1330
  },
1330
1331
  },
1331
1332
  }
1333
+ # Generated from the updated schema.yaml from 0035208, which includes unpublished bugfixes
1334
+ # https://github.com/OAI/OpenAPI-Specification/blob/0035208611701b4f7f2c959eb99a8725cca41e6e/schemas/v3.1/schema.yaml
1332
1335
  OPENAPI_31 = {
1333
1336
  "$id": "https://spec.openapis.org/oas/3.1/schema/2022-10-07",
1334
1337
  "$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1344,7 +1347,7 @@ OPENAPI_31 = {
1344
1347
  },
1345
1348
  "servers": {"type": "array", "items": {"$ref": "#/$defs/server"}, "default": [{"url": "/"}]},
1346
1349
  "paths": {"$ref": "#/$defs/paths"},
1347
- "webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item-or-reference"}},
1350
+ "webhooks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
1348
1351
  "components": {"$ref": "#/$defs/components"},
1349
1352
  "security": {"type": "array", "items": {"$ref": "#/$defs/security-requirement"}},
1350
1353
  "tags": {"type": "array", "items": {"$ref": "#/$defs/tag"}},
@@ -1399,7 +1402,7 @@ OPENAPI_31 = {
1399
1402
  "$comment": "https://spec.openapis.org/oas/v3.1.0#server-object",
1400
1403
  "type": "object",
1401
1404
  "properties": {
1402
- "url": {"type": "string", "format": "uri-reference"},
1405
+ "url": {"type": "string"},
1403
1406
  "description": {"type": "string"},
1404
1407
  "variables": {"type": "object", "additionalProperties": {"$ref": "#/$defs/server-variable"}},
1405
1408
  },
@@ -1438,7 +1441,7 @@ OPENAPI_31 = {
1438
1441
  },
1439
1442
  "links": {"type": "object", "additionalProperties": {"$ref": "#/$defs/link-or-reference"}},
1440
1443
  "callbacks": {"type": "object", "additionalProperties": {"$ref": "#/$defs/callbacks-or-reference"}},
1441
- "pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item-or-reference"}},
1444
+ "pathItems": {"type": "object", "additionalProperties": {"$ref": "#/$defs/path-item"}},
1442
1445
  },
1443
1446
  "patternProperties": {
1444
1447
  "^(schemas|responses|parameters|examples|requestBodies|headers|securitySchemes|links|callbacks|pathItems)$": {
@@ -1460,6 +1463,7 @@ OPENAPI_31 = {
1460
1463
  "$comment": "https://spec.openapis.org/oas/v3.1.0#path-item-object",
1461
1464
  "type": "object",
1462
1465
  "properties": {
1466
+ "$ref": {"type": "string", "format": "uri-reference"},
1463
1467
  "summary": {"type": "string"},
1464
1468
  "description": {"type": "string"},
1465
1469
  "servers": {"type": "array", "items": {"$ref": "#/$defs/server"}},
@@ -1476,11 +1480,6 @@ OPENAPI_31 = {
1476
1480
  "$ref": "#/$defs/specification-extensions",
1477
1481
  "unevaluatedProperties": False,
1478
1482
  },
1479
- "path-item-or-reference": {
1480
- "if": {"type": "object", "required": ["$ref"]},
1481
- "then": {"$ref": "#/$defs/reference"},
1482
- "else": {"$ref": "#/$defs/path-item"},
1483
- },
1484
1483
  "operation": {
1485
1484
  "$comment": "https://spec.openapis.org/oas/v3.1.0#operation-object",
1486
1485
  "type": "object",
@@ -1541,7 +1540,6 @@ OPENAPI_31 = {
1541
1540
  "if": {"properties": {"in": {"const": "path"}}, "required": ["in"]},
1542
1541
  "then": {
1543
1542
  "properties": {
1544
- "name": {"pattern": "[^/#?]+$"},
1545
1543
  "style": {"default": "simple", "enum": ["matrix", "label", "simple"]},
1546
1544
  "required": {"const": True},
1547
1545
  },
@@ -1661,7 +1659,7 @@ OPENAPI_31 = {
1661
1659
  "$comment": "https://spec.openapis.org/oas/v3.1.0#callback-object",
1662
1660
  "type": "object",
1663
1661
  "$ref": "#/$defs/specification-extensions",
1664
- "additionalProperties": {"$ref": "#/$defs/path-item-or-reference"},
1662
+ "additionalProperties": {"$ref": "#/$defs/path-item"},
1665
1663
  },
1666
1664
  "callbacks-or-reference": {
1667
1665
  "if": {"type": "object", "required": ["$ref"]},
@@ -1754,7 +1752,6 @@ OPENAPI_31 = {
1754
1752
  "summary": {"type": "string"},
1755
1753
  "description": {"type": "string"},
1756
1754
  },
1757
- "unevaluatedProperties": False,
1758
1755
  },
1759
1756
  "schema": {
1760
1757
  "$comment": "https://spec.openapis.org/oas/v3.1.0#schema-object",
@@ -1906,11 +1903,7 @@ _VALIDATORS = [
1906
1903
  "OPENAPI_31_VALIDATOR",
1907
1904
  ]
1908
1905
 
1909
- __all__ = [
1910
- "SWAGGER_20",
1911
- "OPENAPI_30",
1912
- "OPENAPI_31",
1913
- ] + _VALIDATORS
1906
+ __all__ = ["SWAGGER_20", "OPENAPI_30", "OPENAPI_31", *_VALIDATORS]
1914
1907
 
1915
1908
  _imports = {
1916
1909
  "SWAGGER_20_VALIDATOR": lambda: make_validator(SWAGGER_20),
@@ -3,19 +3,27 @@ from __future__ import annotations
3
3
  from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
- from itertools import islice, cycle, chain
7
- from typing import Any, Generator, Union, cast
6
+ from itertools import chain, cycle, islice
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
8
8
 
9
9
  import requests
10
- import hypothesis
11
10
  from hypothesis_jsonschema import from_schema
12
- from hypothesis.strategies import SearchStrategy
13
11
 
14
- from .parameters import OpenAPIParameter, OpenAPIBody
15
- from ...constants import DEFAULT_RESPONSE_TIMEOUT
16
- from ...models import APIOperation, Case
17
- from ._hypothesis import get_case_strategy
12
+ from schemathesis.core.transforms import deepclone
13
+ from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
14
+ from schemathesis.generation import GenerationConfig
15
+ from schemathesis.generation.case import Case
16
+ from schemathesis.generation.hypothesis import examples
17
+ from schemathesis.generation.meta import TestPhase
18
+ from schemathesis.schemas import APIOperation
19
+
20
+ from ._hypothesis import get_default_format_strategies, openapi_cases
18
21
  from .constants import LOCATION_TO_CONTAINER
22
+ from .formats import STRING_FORMATS
23
+ from .parameters import OpenAPIBody, OpenAPIParameter
24
+
25
+ if TYPE_CHECKING:
26
+ from hypothesis.strategies import SearchStrategy
19
27
 
20
28
 
21
29
  @dataclass
@@ -39,7 +47,7 @@ Example = Union[ParameterExample, BodyExample]
39
47
 
40
48
 
41
49
  def get_strategies_from_examples(
42
- operation: APIOperation[OpenAPIParameter, Case], examples_field: str = "examples"
50
+ operation: APIOperation[OpenAPIParameter], **kwargs: Any
43
51
  ) -> list[SearchStrategy[Case]]:
44
52
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
45
53
  maps = {}
@@ -63,13 +71,16 @@ def get_strategies_from_examples(
63
71
  # Add examples from parameter's schemas
64
72
  examples.extend(extract_from_schemas(operation))
65
73
  return [
66
- get_case_strategy(operation=operation, **parameters).map(serialize_components)
74
+ openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXPLICIT}).map(
75
+ serialize_components
76
+ )
67
77
  for parameters in produce_combinations(examples)
68
78
  ]
69
79
 
70
80
 
71
- def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
81
+ def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
72
82
  """Extract top-level parameter examples from `examples` & `example` fields."""
83
+ responses = find_in_responses(operation)
73
84
  for parameter in operation.iter_parameters():
74
85
  if "schema" in parameter.definition:
75
86
  definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
@@ -99,6 +110,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
99
110
  yield ParameterExample(
100
111
  container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
101
112
  )
113
+ for value in find_matching_in_responses(responses, parameter.name):
114
+ yield ParameterExample(
115
+ container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
116
+ )
102
117
  for alternative in operation.body:
103
118
  alternative = cast(OpenAPIBody, alternative)
104
119
  if "schema" in alternative.definition:
@@ -126,14 +141,30 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
126
141
  def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
127
142
  yield schema
128
143
  if isinstance(schema, dict):
129
- for key in ("anyOf", "oneOf", "allOf"):
144
+ for key in ("anyOf", "oneOf"):
130
145
  if key in schema:
131
146
  for subschema in schema[key]:
132
147
  yield subschema
148
+ if "allOf" in schema:
149
+ subschema = deepclone(schema["allOf"][0])
150
+ for sub in schema["allOf"][1:]:
151
+ if isinstance(sub, dict):
152
+ for key, value in sub.items():
153
+ if key == "properties":
154
+ subschema.setdefault("properties", {}).update(value)
155
+ elif key == "required":
156
+ subschema.setdefault("required", []).extend(value)
157
+ elif key == "examples":
158
+ subschema.setdefault("examples", []).extend(value)
159
+ elif key == "example":
160
+ subschema.setdefault("examples", []).append(value)
161
+ else:
162
+ subschema[key] = value
163
+ yield subschema
133
164
 
134
165
 
135
166
  def _find_parameter_examples_definition(
136
- operation: APIOperation[OpenAPIParameter, Case], parameter_name: str, field_name: str
167
+ operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
137
168
  ) -> dict[str, Any]:
138
169
  """Find the original, unresolved `examples` definition of a parameter."""
139
170
  from .schemas import BaseOpenAPISchema
@@ -151,13 +182,13 @@ def _find_parameter_examples_definition(
151
182
 
152
183
 
153
184
  def _find_request_body_examples_definition(
154
- operation: APIOperation[OpenAPIParameter, Case], alternative: OpenAPIBody
185
+ operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
155
186
  ) -> dict[str, Any]:
156
187
  """Find the original, unresolved `examples` definition of a request body variant."""
157
188
  from .schemas import BaseOpenAPISchema
158
189
 
159
190
  schema = cast(BaseOpenAPISchema, operation.schema)
160
- if schema.spec_version == "2.0":
191
+ if schema.specification.version == "2.0":
161
192
  raw_schema = schema.raw_schema
162
193
  path_data = raw_schema["paths"][operation.path]
163
194
  parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
@@ -178,7 +209,7 @@ def extract_inner_examples(
178
209
  ) -> Generator[Any, None, None]:
179
210
  """Extract exact examples values from the `examples` dictionary."""
180
211
  for name, example in examples.items():
181
- if "$ref" in unresolved_definition[name]:
212
+ if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
182
213
  # The example here is a resolved example and should be yielded as is
183
214
  yield example
184
215
  if isinstance(example, dict):
@@ -193,12 +224,12 @@ def extract_inner_examples(
193
224
  @lru_cache
194
225
  def load_external_example(url: str) -> bytes:
195
226
  """Load examples the `externalValue` keyword."""
196
- response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
227
+ response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
197
228
  response.raise_for_status()
198
229
  return response.content
199
230
 
200
231
 
201
- def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
232
+ def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
202
233
  """Extract examples from parameters' schema definitions."""
203
234
  for parameter in operation.iter_parameters():
204
235
  schema = parameter.as_json_schema(operation)
@@ -209,12 +240,13 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
209
240
  for alternative in operation.body:
210
241
  alternative = cast(OpenAPIBody, alternative)
211
242
  schema = alternative.as_json_schema(operation)
212
- for value in extract_from_schema(operation, schema, alternative.example_field, alternative.examples_field):
213
- yield BodyExample(value=value, media_type=alternative.media_type)
243
+ for example_field, examples_field in (("example", "examples"), ("x-example", "x-examples")):
244
+ for value in extract_from_schema(operation, schema, example_field, examples_field):
245
+ yield BodyExample(value=value, media_type=alternative.media_type)
214
246
 
215
247
 
216
248
  def extract_from_schema(
217
- operation: APIOperation[OpenAPIParameter, Case],
249
+ operation: APIOperation[OpenAPIParameter],
218
250
  schema: dict[str, Any],
219
251
  example_field_name: str,
220
252
  examples_field_name: str,
@@ -236,6 +268,8 @@ def extract_from_schema(
236
268
  if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
237
269
  # These are JSON Schema examples, which is an array of values
238
270
  values.extend(subsubschema[examples_field_name])
271
+ # Check nested examples as well
272
+ values.extend(extract_from_schema(operation, subsubschema, example_field_name, examples_field_name))
239
273
  if not values:
240
274
  if name in required:
241
275
  # Defer generation to only generate these variants if at least one property has examples
@@ -248,7 +282,7 @@ def extract_from_schema(
248
282
  # Generated by one of `anyOf` or similar sub-schemas
249
283
  continue
250
284
  subschema = operation.schema.prepare_schema(subschema)
251
- generated = _generate_single_example(subschema)
285
+ generated = _generate_single_example(subschema, operation.schema.generation_config)
252
286
  variants[name] = [generated]
253
287
  # Calculate the maximum number of examples any property has
254
288
  total_combos = max(len(examples) for examples in variants.values())
@@ -264,24 +298,17 @@ def extract_from_schema(
264
298
  yield [value]
265
299
 
266
300
 
267
- def _generate_single_example(schema: dict[str, Any]) -> Any:
268
- examples = []
269
-
270
- @hypothesis.given(from_schema(schema)) # type: ignore
271
- @hypothesis.settings( # type: ignore
272
- database=None,
273
- max_examples=1,
274
- deadline=None,
275
- verbosity=hypothesis.Verbosity.quiet,
276
- phases=(hypothesis.Phase.generate,),
277
- suppress_health_check=list(hypothesis.HealthCheck),
301
+ def _generate_single_example(
302
+ schema: dict[str, Any],
303
+ generation_config: GenerationConfig,
304
+ ) -> Any:
305
+ strategy = from_schema(
306
+ schema,
307
+ custom_formats={**get_default_format_strategies(), **STRING_FORMATS},
308
+ allow_x00=generation_config.allow_x00,
309
+ codec=generation_config.codec,
278
310
  )
279
- def example_generating_inner_function(ex: Any) -> None:
280
- examples.append(ex)
281
-
282
- example_generating_inner_function()
283
-
284
- return examples[0]
311
+ return examples.generate_one(strategy)
285
312
 
286
313
 
287
314
  def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
@@ -330,3 +357,90 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
330
357
  }
331
358
  for container, variants in parameters.items()
332
359
  }
360
+
361
+
362
+ def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
363
+ """Find schema examples in responses."""
364
+ output: dict[str, list[dict[str, Any]]] = {}
365
+ for status_code, response in operation.definition.raw.get("responses", {}).items():
366
+ if not str(status_code).startswith("2"):
367
+ # Check only 2xx responses
368
+ continue
369
+ if isinstance(response, dict) and "$ref" in response:
370
+ _, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
371
+ for media_type, definition in response.get("content", {}).items():
372
+ schema_ref = definition.get("schema", {}).get("$ref")
373
+ if schema_ref:
374
+ name = schema_ref.split("/")[-1]
375
+ else:
376
+ name = f"{status_code}/{media_type}"
377
+ for examples_field, example_field in (
378
+ ("examples", "example"),
379
+ ("x-examples", "x-example"),
380
+ ):
381
+ examples = definition.get(examples_field, {})
382
+ for example in examples.values():
383
+ if "value" in example:
384
+ output.setdefault(name, []).append(example["value"])
385
+ if example_field in definition:
386
+ output.setdefault(name, []).append(definition[example_field])
387
+ return output
388
+
389
+
390
+ NOT_FOUND = object()
391
+
392
+
393
+ def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
394
+ """Find matching parameter examples."""
395
+ normalized = param.lower()
396
+ is_id_param = normalized.endswith("id")
397
+ # Extract values from response examples that match input parameters.
398
+ # E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
399
+ # as examples for the "id" path parameter.
400
+ for schema_name, schema_examples in examples.items():
401
+ for example in schema_examples:
402
+ if not isinstance(example, dict):
403
+ continue
404
+ # Unwrapping example from `{"item": [{...}]}`
405
+ if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
406
+ inner = list(example.values())[0]
407
+ if isinstance(inner, list):
408
+ for sub_example in inner:
409
+ found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
410
+ if found is not NOT_FOUND:
411
+ yield found
412
+ continue
413
+ if isinstance(inner, dict):
414
+ example = inner
415
+ found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
416
+ if found is not NOT_FOUND:
417
+ yield found
418
+
419
+
420
+ def _find_matching_in_responses(
421
+ example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
422
+ ) -> Any:
423
+ # Check for exact match
424
+ if param in example:
425
+ return example[param]
426
+ if is_id_param and param[:-2] in example:
427
+ return example[param[:-2]]
428
+
429
+ # Check for case-insensitive match
430
+ for key in example:
431
+ if key.lower() == normalized:
432
+ return example[key]
433
+ else:
434
+ # If no match found and it's an ID parameter, try additional checks
435
+ if is_id_param:
436
+ # Check for 'id' if parameter is '{something}Id'
437
+ if "id" in example:
438
+ return example["id"]
439
+ # Check for '{schemaName}Id' or '{schemaName}_id'
440
+ if normalized == "id" or normalized.startswith(schema_name.lower()):
441
+ for key in (schema_name, schema_name.lower()):
442
+ for suffix in ("_id", "Id"):
443
+ with_suffix = f"{key}{suffix}"
444
+ if with_suffix in example:
445
+ return example[with_suffix]
446
+ return NOT_FOUND
@@ -2,14 +2,27 @@
2
2
 
3
3
  https://swagger.io/docs/specification/links/#runtime-expressions
4
4
  """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
5
9
  from typing import Any
6
10
 
7
11
  from . import lexer, nodes, parser
8
12
  from .context import ExpressionContext
9
13
 
14
+ __all__ = [
15
+ "lexer",
16
+ "nodes",
17
+ "parser",
18
+ "ExpressionContext",
19
+ ]
10
20
 
11
- def evaluate(expr: Any, context: ExpressionContext) -> str:
21
+
22
+ def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
12
23
  """Evaluate runtime expression in context."""
24
+ if isinstance(expr, (dict, list)) and evaluate_nested:
25
+ return _evaluate_nested(expr, context)
13
26
  if not isinstance(expr, str):
14
27
  # Can be a non-string constant
15
28
  return expr
@@ -17,4 +30,26 @@ def evaluate(expr: Any, context: ExpressionContext) -> str:
17
30
  if len(parts) == 1:
18
31
  return parts[0] # keep the return type the same as the internal value type
19
32
  # otherwise, concatenate into a string
20
- return "".join(map(str, parts))
33
+ return "".join(str(part) for part in parts if part is not None)
34
+
35
+
36
+ def _evaluate_nested(expr: dict[str, Any] | list, context: ExpressionContext) -> Any:
37
+ if isinstance(expr, dict):
38
+ return {
39
+ _evaluate_object_key(key, context): evaluate(value, context, evaluate_nested=True)
40
+ for key, value in expr.items()
41
+ }
42
+ return [evaluate(item, context, evaluate_nested=True) for item in expr]
43
+
44
+
45
+ def _evaluate_object_key(key: str, context: ExpressionContext) -> Any:
46
+ evaluated = evaluate(key, context)
47
+ if isinstance(evaluated, str):
48
+ return evaluated
49
+ if isinstance(evaluated, bool):
50
+ return "true" if evaluated else "false"
51
+ if isinstance(evaluated, (int, float)):
52
+ return str(evaluated)
53
+ if evaluated is None:
54
+ return "null"
55
+ return json.dumps(evaluated)
@@ -1,16 +1,14 @@
1
1
  from __future__ import annotations
2
- from dataclasses import dataclass
3
- from typing import TYPE_CHECKING
4
2
 
3
+ from dataclasses import dataclass
5
4
 
6
- if TYPE_CHECKING:
7
- from ....models import Case
8
- from ....transports.responses import GenericResponse
5
+ from schemathesis.core.transport import Response
6
+ from schemathesis.generation.case import Case
9
7
 
10
8
 
11
9
  @dataclass
12
10
  class ExpressionContext:
13
11
  """Context in what an expression are evaluated."""
14
12
 
15
- response: GenericResponse
13
+ response: Response
16
14
  case: Case
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class Extractor:
9
+ def extract(self, value: str) -> str | None:
10
+ raise NotImplementedError
11
+
12
+
13
+ @dataclass
14
+ class RegexExtractor(Extractor):
15
+ """Extract value via a regex."""
16
+
17
+ value: re.Pattern
18
+
19
+ def extract(self, value: str) -> str | None:
20
+ match = self.value.search(value)
21
+ if match is None:
22
+ return None
23
+ return match.group(1)
@@ -1,4 +1,5 @@
1
1
  """Lexical analysis of runtime expressions."""
2
+
2
3
  from dataclasses import dataclass
3
4
  from enum import Enum, unique
4
5
  from typing import Callable, Generator
@@ -19,33 +20,34 @@ class Token:
19
20
  """Lexical token that may occur in a runtime expression."""
20
21
 
21
22
  value: str
23
+ end: int
22
24
  type_: TokenType
23
25
 
24
26
  # Helpers for cleaner instantiation
25
27
 
26
28
  @classmethod
27
- def variable(cls, value: str) -> "Token":
28
- return cls(value, TokenType.VARIABLE)
29
+ def variable(cls, value: str, end: int) -> "Token":
30
+ return cls(value, end, TokenType.VARIABLE)
29
31
 
30
32
  @classmethod
31
- def string(cls, value: str) -> "Token":
32
- return cls(value, TokenType.STRING)
33
+ def string(cls, value: str, end: int) -> "Token":
34
+ return cls(value, end, TokenType.STRING)
33
35
 
34
36
  @classmethod
35
- def pointer(cls, value: str) -> "Token":
36
- return cls(value, TokenType.POINTER)
37
+ def pointer(cls, value: str, end: int) -> "Token":
38
+ return cls(value, end, TokenType.POINTER)
37
39
 
38
40
  @classmethod
39
- def lbracket(cls) -> "Token":
40
- return cls("{", TokenType.LBRACKET)
41
+ def lbracket(cls, end: int) -> "Token":
42
+ return cls("{", end, TokenType.LBRACKET)
41
43
 
42
44
  @classmethod
43
- def rbracket(cls) -> "Token":
44
- return cls("}", TokenType.RBRACKET)
45
+ def rbracket(cls, end: int) -> "Token":
46
+ return cls("}", end, TokenType.RBRACKET)
45
47
 
46
48
  @classmethod
47
- def dot(cls) -> "Token":
48
- return cls(".", TokenType.DOT)
49
+ def dot(cls, end: int) -> "Token":
50
+ return cls(".", end, TokenType.DOT)
49
51
 
50
52
  # Helpers for simpler type comparison
51
53
 
@@ -102,15 +104,15 @@ def tokenize(expression: str) -> TokenGenerator:
102
104
  if current_symbol() == "$":
103
105
  start = cursor
104
106
  move_until(lambda: is_eol() or current_symbol() in stop_symbols)
105
- yield Token.variable(expression[start:cursor])
107
+ yield Token.variable(expression[start:cursor], cursor - 1)
106
108
  elif current_symbol() == ".":
107
- yield Token.dot()
109
+ yield Token.dot(cursor)
108
110
  move()
109
111
  elif current_symbol() == "{":
110
- yield Token.lbracket()
112
+ yield Token.lbracket(cursor)
111
113
  move()
112
114
  elif current_symbol() == "}":
113
- yield Token.rbracket()
115
+ yield Token.rbracket(cursor)
114
116
  move()
115
117
  elif current_symbol() == "#":
116
118
  start = cursor
@@ -125,8 +127,8 @@ def tokenize(expression: str) -> TokenGenerator:
125
127
  # `ID_{$response.body#/foo}_{$response.body#/bar}`
126
128
  # Which is much easier if we treat `}` as a closing bracket of an embedded runtime expression
127
129
  move_until(lambda: is_eol() or current_symbol() == "}")
128
- yield Token.pointer(expression[start:cursor])
130
+ yield Token.pointer(expression[start:cursor], cursor - 1)
129
131
  else:
130
132
  start = cursor
131
133
  move_until(lambda: is_eol() or current_symbol() in stop_symbols)
132
- yield Token.string(expression[start:cursor])
134
+ yield Token.string(expression[start:cursor], cursor - 1)