schemathesis 3.25.6__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 -1760
  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/{runner → engine/phases}/probes.py +50 -67
  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 +139 -23
  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 +478 -369
  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.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.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 -58
  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 -790
  156. schemathesis/cli/output/short.py +0 -44
  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 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  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 -323
  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 -199
  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.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.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),
@@ -4,23 +4,26 @@ from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
6
  from itertools import chain, cycle, islice
7
- from typing import TYPE_CHECKING, Any, Generator, Union, cast
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
8
8
 
9
9
  import requests
10
- from hypothesis.strategies import SearchStrategy
11
10
  from hypothesis_jsonschema import from_schema
12
11
 
13
- from ...constants import DEFAULT_RESPONSE_TIMEOUT
14
- from ...models import APIOperation, Case
15
- from ..._hypothesis import get_single_example
16
- from ._hypothesis import get_case_strategy, get_default_format_strategies
17
- from .formats import STRING_FORMATS
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
19
23
  from .parameters import OpenAPIBody, OpenAPIParameter
20
24
 
21
-
22
25
  if TYPE_CHECKING:
23
- from ...generation import GenerationConfig
26
+ from hypothesis.strategies import SearchStrategy
24
27
 
25
28
 
26
29
  @dataclass
@@ -44,7 +47,7 @@ Example = Union[ParameterExample, BodyExample]
44
47
 
45
48
 
46
49
  def get_strategies_from_examples(
47
- operation: APIOperation[OpenAPIParameter, Case], examples_field: str = "examples"
50
+ operation: APIOperation[OpenAPIParameter], **kwargs: Any
48
51
  ) -> list[SearchStrategy[Case]]:
49
52
  """Build a set of strategies that generate test cases based on explicit examples in the schema."""
50
53
  maps = {}
@@ -68,13 +71,16 @@ def get_strategies_from_examples(
68
71
  # Add examples from parameter's schemas
69
72
  examples.extend(extract_from_schemas(operation))
70
73
  return [
71
- get_case_strategy(operation=operation, **parameters).map(serialize_components)
74
+ openapi_cases(operation=operation, **{**parameters, **kwargs, "phase": TestPhase.EXPLICIT}).map(
75
+ serialize_components
76
+ )
72
77
  for parameters in produce_combinations(examples)
73
78
  ]
74
79
 
75
80
 
76
- def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
81
+ def extract_top_level(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
77
82
  """Extract top-level parameter examples from `examples` & `example` fields."""
83
+ responses = find_in_responses(operation)
78
84
  for parameter in operation.iter_parameters():
79
85
  if "schema" in parameter.definition:
80
86
  definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
@@ -104,6 +110,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
104
110
  yield ParameterExample(
105
111
  container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
106
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
+ )
107
117
  for alternative in operation.body:
108
118
  alternative = cast(OpenAPIBody, alternative)
109
119
  if "schema" in alternative.definition:
@@ -131,14 +141,30 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
131
141
  def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any] | bool, None, None]:
132
142
  yield schema
133
143
  if isinstance(schema, dict):
134
- for key in ("anyOf", "oneOf", "allOf"):
144
+ for key in ("anyOf", "oneOf"):
135
145
  if key in schema:
136
146
  for subschema in schema[key]:
137
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
138
164
 
139
165
 
140
166
  def _find_parameter_examples_definition(
141
- operation: APIOperation[OpenAPIParameter, Case], parameter_name: str, field_name: str
167
+ operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
142
168
  ) -> dict[str, Any]:
143
169
  """Find the original, unresolved `examples` definition of a parameter."""
144
170
  from .schemas import BaseOpenAPISchema
@@ -156,13 +182,13 @@ def _find_parameter_examples_definition(
156
182
 
157
183
 
158
184
  def _find_request_body_examples_definition(
159
- operation: APIOperation[OpenAPIParameter, Case], alternative: OpenAPIBody
185
+ operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
160
186
  ) -> dict[str, Any]:
161
187
  """Find the original, unresolved `examples` definition of a request body variant."""
162
188
  from .schemas import BaseOpenAPISchema
163
189
 
164
190
  schema = cast(BaseOpenAPISchema, operation.schema)
165
- if schema.spec_version == "2.0":
191
+ if schema.specification.version == "2.0":
166
192
  raw_schema = schema.raw_schema
167
193
  path_data = raw_schema["paths"][operation.path]
168
194
  parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
@@ -183,7 +209,7 @@ def extract_inner_examples(
183
209
  ) -> Generator[Any, None, None]:
184
210
  """Extract exact examples values from the `examples` dictionary."""
185
211
  for name, example in examples.items():
186
- if "$ref" in unresolved_definition[name]:
212
+ if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
187
213
  # The example here is a resolved example and should be yielded as is
188
214
  yield example
189
215
  if isinstance(example, dict):
@@ -198,12 +224,12 @@ def extract_inner_examples(
198
224
  @lru_cache
199
225
  def load_external_example(url: str) -> bytes:
200
226
  """Load examples the `externalValue` keyword."""
201
- response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT / 1000)
227
+ response = requests.get(url, timeout=DEFAULT_RESPONSE_TIMEOUT)
202
228
  response.raise_for_status()
203
229
  return response.content
204
230
 
205
231
 
206
- def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
232
+ def extract_from_schemas(operation: APIOperation[OpenAPIParameter]) -> Generator[Example, None, None]:
207
233
  """Extract examples from parameters' schema definitions."""
208
234
  for parameter in operation.iter_parameters():
209
235
  schema = parameter.as_json_schema(operation)
@@ -214,12 +240,13 @@ def extract_from_schemas(operation: APIOperation[OpenAPIParameter, Case]) -> Gen
214
240
  for alternative in operation.body:
215
241
  alternative = cast(OpenAPIBody, alternative)
216
242
  schema = alternative.as_json_schema(operation)
217
- for value in extract_from_schema(operation, schema, alternative.example_field, alternative.examples_field):
218
- 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)
219
246
 
220
247
 
221
248
  def extract_from_schema(
222
- operation: APIOperation[OpenAPIParameter, Case],
249
+ operation: APIOperation[OpenAPIParameter],
223
250
  schema: dict[str, Any],
224
251
  example_field_name: str,
225
252
  examples_field_name: str,
@@ -241,6 +268,8 @@ def extract_from_schema(
241
268
  if examples_field_name in subsubschema and isinstance(subsubschema[examples_field_name], list):
242
269
  # These are JSON Schema examples, which is an array of values
243
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))
244
273
  if not values:
245
274
  if name in required:
246
275
  # Defer generation to only generate these variants if at least one property has examples
@@ -279,7 +308,7 @@ def _generate_single_example(
279
308
  allow_x00=generation_config.allow_x00,
280
309
  codec=generation_config.codec,
281
310
  )
282
- return get_single_example(strategy)
311
+ return examples.generate_one(strategy)
283
312
 
284
313
 
285
314
  def produce_combinations(examples: list[Example]) -> Generator[dict[str, Any], None, None]:
@@ -328,3 +357,90 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
328
357
  }
329
358
  for container, variants in parameters.items()
330
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)