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,37 +1,41 @@
1
1
  from __future__ import annotations
2
+
2
3
  import io
3
4
  import json
4
5
  import pathlib
5
6
  import re
6
- from typing import IO, Any, Callable, cast, TYPE_CHECKING
7
+ from typing import IO, TYPE_CHECKING, Any, Callable, cast
7
8
  from urllib.parse import urljoin
8
9
 
9
10
  from ... import experimental, fixups
10
11
  from ...code_samples import CodeSampleStyle
12
+ from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
13
+ from ...exceptions import SchemaError, SchemaErrorType
14
+ from ...filters import filter_set_from_components
11
15
  from ...generation import (
12
16
  DEFAULT_DATA_GENERATION_METHODS,
13
- DataGenerationMethodInput,
14
17
  DataGenerationMethod,
18
+ DataGenerationMethodInput,
15
19
  GenerationConfig,
16
20
  )
17
- from ...constants import WAIT_FOR_SCHEMA_INTERVAL
18
- from ...exceptions import SchemaError, SchemaErrorType
19
21
  from ...hooks import HookContext, dispatch
22
+ from ...internal.deprecation import warn_filtration_arguments
23
+ from ...internal.output import OutputConfig
24
+ from ...internal.validation import require_relative_url
20
25
  from ...loaders import load_schema_from_url, load_yaml
21
26
  from ...throttling import build_limiter
22
- from ...types import Filter, NotSet, PathLike
23
27
  from ...transports.content_types import is_json_media_type, is_yaml_media_type
24
28
  from ...transports.headers import setup_default_headers
25
- from ...internal.validation import require_relative_url
26
- from ...constants import NOT_SET
29
+ from ...types import Filter, NotSet, PathLike, Specification
27
30
  from . import definitions, validation
28
31
 
29
32
  if TYPE_CHECKING:
30
- from .schemas import BaseOpenAPISchema
31
- from ...transports.responses import GenericResponse
32
33
  import jsonschema
33
34
  from pyrate_limiter import Limiter
35
+
34
36
  from ...lazy import LazySchema
37
+ from ...transports.responses import GenericResponse
38
+ from .schemas import BaseOpenAPISchema
35
39
 
36
40
 
37
41
  def _is_json_response(response: GenericResponse) -> bool:
@@ -73,11 +77,12 @@ def from_path(
73
77
  endpoint: Filter | None = None,
74
78
  tag: Filter | None = None,
75
79
  operation_id: Filter | None = None,
76
- skip_deprecated_operations: bool = False,
80
+ skip_deprecated_operations: bool | None = None,
77
81
  validate_schema: bool = False,
78
82
  force_schema_version: str | None = None,
79
83
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
80
84
  generation_config: GenerationConfig | None = None,
85
+ output_config: OutputConfig | None = None,
81
86
  code_sample_style: str = CodeSampleStyle.default().name,
82
87
  rate_limit: str | None = None,
83
88
  encoding: str = "utf8",
@@ -102,6 +107,7 @@ def from_path(
102
107
  force_schema_version=force_schema_version,
103
108
  data_generation_methods=data_generation_methods,
104
109
  generation_config=generation_config,
110
+ output_config=output_config,
105
111
  code_sample_style=code_sample_style,
106
112
  location=pathlib.Path(path).absolute().as_uri(),
107
113
  rate_limit=rate_limit,
@@ -121,11 +127,12 @@ def from_uri(
121
127
  endpoint: Filter | None = None,
122
128
  tag: Filter | None = None,
123
129
  operation_id: Filter | None = None,
124
- skip_deprecated_operations: bool = False,
130
+ skip_deprecated_operations: bool | None = None,
125
131
  validate_schema: bool = False,
126
132
  force_schema_version: str | None = None,
127
133
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
128
134
  generation_config: GenerationConfig | None = None,
135
+ output_config: OutputConfig | None = None,
129
136
  code_sample_style: str = CodeSampleStyle.default().name,
130
137
  wait_for_schema: float | None = None,
131
138
  rate_limit: str | None = None,
@@ -156,11 +163,12 @@ def from_uri(
156
163
  interval=WAIT_FOR_SCHEMA_INTERVAL,
157
164
  )
158
165
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
159
- return requests.get(_uri, **kwargs)
166
+ return requests.get(_uri, **_kwargs)
160
167
 
161
168
  else:
162
169
  _load_schema = requests.get
163
170
 
171
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
164
172
  response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
165
173
  return from_file(
166
174
  response.text,
@@ -175,6 +183,7 @@ def from_uri(
175
183
  force_schema_version=force_schema_version,
176
184
  data_generation_methods=data_generation_methods,
177
185
  generation_config=generation_config,
186
+ output_config=output_config,
178
187
  code_sample_style=code_sample_style,
179
188
  location=uri,
180
189
  rate_limit=rate_limit,
@@ -215,11 +224,12 @@ def from_file(
215
224
  endpoint: Filter | None = None,
216
225
  tag: Filter | None = None,
217
226
  operation_id: Filter | None = None,
218
- skip_deprecated_operations: bool = False,
227
+ skip_deprecated_operations: bool | None = None,
219
228
  validate_schema: bool = False,
220
229
  force_schema_version: str | None = None,
221
230
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
222
231
  generation_config: GenerationConfig | None = None,
232
+ output_config: OutputConfig | None = None,
223
233
  code_sample_style: str = CodeSampleStyle.default().name,
224
234
  location: str | None = None,
225
235
  rate_limit: str | None = None,
@@ -266,6 +276,7 @@ def from_file(
266
276
  force_schema_version=force_schema_version,
267
277
  data_generation_methods=data_generation_methods,
268
278
  generation_config=generation_config,
279
+ output_config=output_config,
269
280
  code_sample_style=code_sample_style,
270
281
  location=location,
271
282
  rate_limit=rate_limit,
@@ -289,11 +300,12 @@ def from_dict(
289
300
  endpoint: Filter | None = None,
290
301
  tag: Filter | None = None,
291
302
  operation_id: Filter | None = None,
292
- skip_deprecated_operations: bool = False,
303
+ skip_deprecated_operations: bool | None = None,
293
304
  validate_schema: bool = False,
294
305
  force_schema_version: str | None = None,
295
306
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
296
307
  generation_config: GenerationConfig | None = None,
308
+ output_config: OutputConfig | None = None,
297
309
  code_sample_style: str = CodeSampleStyle.default().name,
298
310
  location: str | None = None,
299
311
  rate_limit: str | None = None,
@@ -303,6 +315,7 @@ def from_dict(
303
315
 
304
316
  :param dict raw_schema: A schema to load.
305
317
  """
318
+ from ... import transports
306
319
  from .schemas import OpenApi30, SwaggerV20
307
320
 
308
321
  if not isinstance(raw_schema, dict):
@@ -320,24 +333,36 @@ def from_dict(
320
333
  if rate_limit is not None:
321
334
  rate_limiter = build_limiter(rate_limit)
322
335
 
336
+ for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
337
+ value = locals()[name]
338
+ if value is not None:
339
+ warn_filtration_arguments(name)
340
+ filter_set = filter_set_from_components(
341
+ include=True,
342
+ method=method,
343
+ endpoint=endpoint,
344
+ tag=tag,
345
+ operation_id=operation_id,
346
+ skip_deprecated_operations=skip_deprecated_operations,
347
+ )
348
+
323
349
  def init_openapi_2() -> SwaggerV20:
324
350
  _maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
325
351
  instance = SwaggerV20(
326
352
  raw_schema,
353
+ specification=Specification.OPENAPI,
327
354
  app=app,
328
355
  base_url=base_url,
329
- method=method,
330
- endpoint=endpoint,
331
- tag=tag,
332
- operation_id=operation_id,
333
- skip_deprecated_operations=skip_deprecated_operations,
356
+ filter_set=filter_set,
334
357
  validate_schema=validate_schema,
335
358
  data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
336
359
  generation_config=generation_config or GenerationConfig(),
360
+ output_config=output_config or OutputConfig(),
337
361
  code_sample_style=_code_sample_style,
338
362
  location=location,
339
363
  rate_limiter=rate_limiter,
340
364
  sanitize_output=sanitize_output,
365
+ transport=transports.get(app),
341
366
  )
342
367
  dispatch("after_load_schema", hook_context, instance)
343
368
  return instance
@@ -365,20 +390,19 @@ def from_dict(
365
390
  _maybe_validate_schema(raw_schema, validator, validate_schema)
366
391
  instance = OpenApi30(
367
392
  raw_schema,
393
+ specification=Specification.OPENAPI,
368
394
  app=app,
369
395
  base_url=base_url,
370
- method=method,
371
- endpoint=endpoint,
372
- tag=tag,
373
- operation_id=operation_id,
374
- skip_deprecated_operations=skip_deprecated_operations,
396
+ filter_set=filter_set,
375
397
  validate_schema=validate_schema,
376
398
  data_generation_methods=DataGenerationMethod.ensure_list(data_generation_methods),
377
399
  generation_config=generation_config or GenerationConfig(),
400
+ output_config=output_config or OutputConfig(),
378
401
  code_sample_style=_code_sample_style,
379
402
  location=location,
380
403
  rate_limiter=rate_limiter,
381
404
  sanitize_output=sanitize_output,
405
+ transport=transports.get(app),
382
406
  )
383
407
  dispatch("after_load_schema", hook_context, instance)
384
408
  return instance
@@ -418,7 +442,7 @@ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str
418
442
  for status_code, path in status_codes:
419
443
  buffer.write(f" - {status_code} at schema['paths']")
420
444
  for chunk in path:
421
- buffer.write(f"[{repr(chunk)}]")
445
+ buffer.write(f"[{chunk!r}]")
422
446
  buffer.write("['responses']\n")
423
447
  return buffer.getvalue().rstrip()
424
448
 
@@ -459,10 +483,11 @@ def from_pytest_fixture(
459
483
  endpoint: Filter | None = NOT_SET,
460
484
  tag: Filter | None = NOT_SET,
461
485
  operation_id: Filter | None = NOT_SET,
462
- skip_deprecated_operations: bool = False,
486
+ skip_deprecated_operations: bool | None = None,
463
487
  validate_schema: bool = False,
464
488
  data_generation_methods: DataGenerationMethodInput | NotSet = NOT_SET,
465
489
  generation_config: GenerationConfig | NotSet = NOT_SET,
490
+ output_config: OutputConfig | NotSet = NOT_SET,
466
491
  code_sample_style: str = CodeSampleStyle.default().name,
467
492
  rate_limit: str | None = None,
468
493
  sanitize_output: bool = True,
@@ -488,18 +513,27 @@ def from_pytest_fixture(
488
513
  rate_limiter: Limiter | None = None
489
514
  if rate_limit is not None:
490
515
  rate_limiter = build_limiter(rate_limit)
491
- return LazySchema(
492
- fixture_name,
493
- app=app,
494
- base_url=base_url,
516
+ for name in ("method", "endpoint", "tag", "operation_id", "skip_deprecated_operations"):
517
+ value = locals()[name]
518
+ if value is not None:
519
+ warn_filtration_arguments(name)
520
+ filter_set = filter_set_from_components(
521
+ include=True,
495
522
  method=method,
496
523
  endpoint=endpoint,
497
524
  tag=tag,
498
525
  operation_id=operation_id,
499
526
  skip_deprecated_operations=skip_deprecated_operations,
527
+ )
528
+ return LazySchema(
529
+ fixture_name,
530
+ app=app,
531
+ base_url=base_url,
532
+ filter_set=filter_set,
500
533
  validate_schema=validate_schema,
501
534
  data_generation_methods=_data_generation_methods,
502
535
  generation_config=generation_config,
536
+ output_config=output_config,
503
537
  code_sample_style=_code_sample_style,
504
538
  rate_limiter=rate_limiter,
505
539
  sanitize_output=sanitize_output,
@@ -515,11 +549,12 @@ def from_wsgi(
515
549
  endpoint: Filter | None = None,
516
550
  tag: Filter | None = None,
517
551
  operation_id: Filter | None = None,
518
- skip_deprecated_operations: bool = False,
552
+ skip_deprecated_operations: bool | None = None,
519
553
  validate_schema: bool = False,
520
554
  force_schema_version: str | None = None,
521
555
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
522
556
  generation_config: GenerationConfig | None = None,
557
+ output_config: OutputConfig | None = None,
523
558
  code_sample_style: str = CodeSampleStyle.default().name,
524
559
  rate_limit: str | None = None,
525
560
  sanitize_output: bool = True,
@@ -530,9 +565,10 @@ def from_wsgi(
530
565
  :param str schema_path: An in-app relative URL to the schema.
531
566
  :param app: A WSGI app instance.
532
567
  """
533
- from ...transports.responses import WSGIResponse
534
568
  from werkzeug.test import Client
535
569
 
570
+ from ...transports.responses import WSGIResponse
571
+
536
572
  require_relative_url(schema_path)
537
573
  setup_default_headers(kwargs)
538
574
  client = Client(app, WSGIResponse)
@@ -550,6 +586,7 @@ def from_wsgi(
550
586
  force_schema_version=force_schema_version,
551
587
  data_generation_methods=data_generation_methods,
552
588
  generation_config=generation_config,
589
+ output_config=output_config,
553
590
  code_sample_style=code_sample_style,
554
591
  location=schema_path,
555
592
  rate_limit=rate_limit,
@@ -559,9 +596,9 @@ def from_wsgi(
559
596
 
560
597
 
561
598
  def get_loader_for_app(app: Any) -> Callable:
562
- from starlette.applications import Starlette
599
+ from ...transports.asgi import is_asgi_app
563
600
 
564
- if isinstance(app, Starlette):
601
+ if is_asgi_app(app):
565
602
  return from_asgi
566
603
  if app.__class__.__module__.startswith("aiohttp."):
567
604
  return from_aiohttp
@@ -577,11 +614,12 @@ def from_aiohttp(
577
614
  endpoint: Filter | None = None,
578
615
  tag: Filter | None = None,
579
616
  operation_id: Filter | None = None,
580
- skip_deprecated_operations: bool = False,
617
+ skip_deprecated_operations: bool | None = None,
581
618
  validate_schema: bool = False,
582
619
  force_schema_version: str | None = None,
583
620
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
584
621
  generation_config: GenerationConfig | None = None,
622
+ output_config: OutputConfig | None = None,
585
623
  code_sample_style: str = CodeSampleStyle.default().name,
586
624
  rate_limit: str | None = None,
587
625
  sanitize_output: bool = True,
@@ -609,6 +647,7 @@ def from_aiohttp(
609
647
  force_schema_version=force_schema_version,
610
648
  data_generation_methods=data_generation_methods,
611
649
  generation_config=generation_config,
650
+ output_config=output_config,
612
651
  code_sample_style=code_sample_style,
613
652
  rate_limit=rate_limit,
614
653
  sanitize_output=sanitize_output,
@@ -625,11 +664,12 @@ def from_asgi(
625
664
  endpoint: Filter | None = None,
626
665
  tag: Filter | None = None,
627
666
  operation_id: Filter | None = None,
628
- skip_deprecated_operations: bool = False,
667
+ skip_deprecated_operations: bool | None = None,
629
668
  validate_schema: bool = False,
630
669
  force_schema_version: str | None = None,
631
670
  data_generation_methods: DataGenerationMethodInput = DEFAULT_DATA_GENERATION_METHODS,
632
671
  generation_config: GenerationConfig | None = None,
672
+ output_config: OutputConfig | None = None,
633
673
  code_sample_style: str = CodeSampleStyle.default().name,
634
674
  rate_limit: str | None = None,
635
675
  sanitize_output: bool = True,
@@ -659,6 +699,7 @@ def from_asgi(
659
699
  force_schema_version=force_schema_version,
660
700
  data_generation_methods=data_generation_methods,
661
701
  generation_config=generation_config,
702
+ output_config=output_config,
662
703
  code_sample_style=code_sample_style,
663
704
  location=schema_path,
664
705
  rate_limit=rate_limit,
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Collection
4
+
5
+ if TYPE_CHECKING:
6
+ from hypothesis import strategies as st
7
+
8
+
9
+ MEDIA_TYPES: dict[str, st.SearchStrategy[bytes]] = {}
10
+
11
+
12
+ def register_media_type(name: str, strategy: st.SearchStrategy[bytes], *, aliases: Collection[str] = ()) -> None:
13
+ """Register a strategy for the given media type."""
14
+ from ...serializers import SerializerContext, register
15
+
16
+ @register(name, aliases=aliases)
17
+ class MediaTypeSerializer:
18
+ def as_requests(self, context: SerializerContext, value: Any) -> dict[str, Any]:
19
+ return {"data": value}
20
+
21
+ def as_werkzeug(self, context: SerializerContext, value: Any) -> dict[str, Any]:
22
+ return {"data": value}
23
+
24
+ MEDIA_TYPES[name] = strategy
25
+ for alias in aliases:
26
+ MEDIA_TYPES[alias] = strategy
27
+
28
+
29
+ def unregister_all() -> None:
30
+ from ...serializers import unregister
31
+
32
+ for media_type in MEDIA_TYPES:
33
+ unregister(media_type)
34
+ MEDIA_TYPES.clear()
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
+
2
3
  from dataclasses import dataclass
3
4
  from functools import lru_cache
4
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
5
6
  from urllib.parse import urlencode
6
7
 
7
8
  import jsonschema
@@ -10,8 +11,10 @@ from hypothesis_jsonschema import from_schema
10
11
 
11
12
  from ..constants import ALL_KEYWORDS
12
13
  from .mutations import MutationContext
13
- from .types import Draw, Schema
14
- from ....generation import GenerationConfig
14
+
15
+ if TYPE_CHECKING:
16
+ from ....generation import GenerationConfig
17
+ from .types import Draw, Schema
15
18
 
16
19
 
17
20
  @dataclass
@@ -1,5 +1,7 @@
1
1
  """Schema mutations."""
2
+
2
3
  from __future__ import annotations
4
+
3
5
  import enum
4
6
  from dataclasses import dataclass
5
7
  from functools import wraps
@@ -79,6 +81,10 @@ class MutationContext:
79
81
  def is_path_location(self) -> bool:
80
82
  return self.location == "path"
81
83
 
84
+ @property
85
+ def is_query_location(self) -> bool:
86
+ return self.location == "query"
87
+
82
88
  def mutate(self, draw: Draw) -> Schema:
83
89
  # On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
84
90
  # taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
@@ -173,7 +179,7 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
173
179
  else:
174
180
  candidate = draw(st.sampled_from(sorted(required)))
175
181
  enabled_properties = draw(st.shared(FeatureStrategy(), key="properties")) # type: ignore
176
- candidates = [candidate] + sorted([prop for prop in required if enabled_properties.is_enabled(prop)])
182
+ candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
177
183
  property_name = draw(st.sampled_from(candidates))
178
184
  required.remove(property_name)
179
185
  if not required:
@@ -201,8 +207,11 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
201
207
  if context.media_type == "application/x-www-form-urlencoded":
202
208
  # Form data should be an object, do not change it
203
209
  return MutationResult.FAILURE
204
- # Headers are always strings, can't negate this
205
- if context.is_header_location:
210
+ # For headers, query and path parameters, if the current type is string, then it already
211
+ # includes all possible values as those parameters will be stringified before sending,
212
+ # therefore it can't be negated.
213
+ types = get_type(schema)
214
+ if "string" in types and (context.is_header_location or context.is_path_location or context.is_query_location):
206
215
  return MutationResult.FAILURE
207
216
  candidates = _get_type_candidates(context, schema)
208
217
  if not candidates:
@@ -217,9 +226,10 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
217
226
  candidate = draw(st.sampled_from(sorted(candidates)))
218
227
  candidates.remove(candidate)
219
228
  enabled_types = draw(st.shared(FeatureStrategy(), key="types")) # type: ignore
220
- remaining_candidates = [candidate] + sorted(
221
- [candidate for candidate in candidates if enabled_types.is_enabled(candidate)]
222
- )
229
+ remaining_candidates = [
230
+ candidate,
231
+ *sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
232
+ ]
223
233
  new_type = draw(st.sampled_from(remaining_candidates))
224
234
  schema["type"] = new_type
225
235
  prevent_unsatisfiable_schema(schema, new_type)
@@ -362,6 +372,11 @@ def negate_constraints(context: MutationContext, draw: Draw, schema: Schema) ->
362
372
  # Should we negate this key?
363
373
  if k == "required":
364
374
  return v != []
375
+ if k in ("example", "examples"):
376
+ return False
377
+ if context.is_path_location and k == "minLength" and v == 1:
378
+ # Empty path parameter will be filtered out
379
+ return False
365
380
  return not (
366
381
  k in ("type", "properties", "items", "minItems")
367
382
  or (k == "additionalProperties" and context.is_header_location)
@@ -1,13 +1,16 @@
1
1
  from __future__ import annotations
2
+
2
3
  import json
3
4
  from dataclasses import dataclass
4
- from typing import Any, ClassVar, Iterable
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Iterable
5
6
 
6
7
  from ...exceptions import OperationSchemaError
7
- from ...models import APIOperation
8
8
  from ...parameters import Parameter
9
9
  from .converter import to_json_schema_recursive
10
10
 
11
+ if TYPE_CHECKING:
12
+ from ...models import APIOperation
13
+
11
14
 
12
15
  @dataclass(eq=False)
13
16
  class OpenAPIParameter(Parameter):
@@ -18,6 +21,7 @@ class OpenAPIParameter(Parameter):
18
21
  nullable_field: ClassVar[str]
19
22
  supported_jsonschema_keywords: ClassVar[tuple[str, ...]]
20
23
 
24
+ def _repr_pretty_(self, *args: Any, **kwargs: Any) -> None: ...
21
25
  @property
22
26
  def description(self) -> str | None:
23
27
  """A brief parameter description."""
@@ -47,16 +51,26 @@ class OpenAPIParameter(Parameter):
47
51
 
48
52
  @property
49
53
  def is_header(self) -> bool:
50
- raise NotImplementedError
54
+ return self.location in ("header", "cookie")
51
55
 
52
- def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
56
+ def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
53
57
  """Convert parameter's definition to JSON Schema."""
58
+ # JSON Schema allows `examples` as an array
59
+ examples = []
60
+ if self.examples_field in self.definition:
61
+ examples.extend(
62
+ [example["value"] for example in self.definition[self.examples_field].values() if "value" in example]
63
+ )
64
+ if self.example_field in self.definition:
65
+ examples.append(self.definition[self.example_field])
54
66
  schema = self.from_open_api_to_json_schema(operation, self.definition)
55
- return self.transform_keywords(schema)
67
+ if examples:
68
+ schema["examples"] = examples
69
+ return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
56
70
 
57
- def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
71
+ def transform_keywords(self, schema: dict[str, Any], *, update_quantifiers: bool = True) -> dict[str, Any]:
58
72
  """Transform Open API specific keywords into JSON Schema compatible form."""
59
- definition = to_json_schema_recursive(schema, self.nullable_field)
73
+ definition = to_json_schema_recursive(schema, self.nullable_field, update_quantifiers=update_quantifiers)
60
74
  # Headers are strings, but it is not always explicitly defined in the schema. By preparing them properly, we
61
75
  # can achieve significant performance improvements for such cases.
62
76
  # For reference (my machine) - running a single test with 100 examples with the resulting strategy:
@@ -116,12 +130,10 @@ class OpenAPI20Parameter(OpenAPIParameter):
116
130
  "uniqueItems",
117
131
  "enum",
118
132
  "multipleOf",
133
+ "example",
134
+ "examples",
119
135
  )
120
136
 
121
- @property
122
- def is_header(self) -> bool:
123
- return self.location == "header"
124
-
125
137
 
126
138
  @dataclass(eq=False)
127
139
  class OpenAPI30Parameter(OpenAPIParameter):
@@ -162,12 +174,10 @@ class OpenAPI30Parameter(OpenAPIParameter):
162
174
  "properties",
163
175
  "additionalProperties",
164
176
  "format",
177
+ "example",
178
+ "examples",
165
179
  )
166
180
 
167
- @property
168
- def is_header(self) -> bool:
169
- return self.location in ("header", "cookie")
170
-
171
181
  def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
172
182
  open_api_schema = get_parameter_schema(operation, open_api_schema)
173
183
  return super().from_open_api_to_json_schema(operation, open_api_schema)
@@ -216,15 +226,17 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
216
226
  "allOf",
217
227
  "properties",
218
228
  "additionalProperties",
229
+ "example",
230
+ "examples",
219
231
  )
220
232
  # NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
221
233
  # the precedence rules consistent.
222
234
 
223
- def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
235
+ def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
224
236
  """Convert body definition to JSON Schema."""
225
237
  # `schema` is required in Open API 2.0 when the `in` keyword is `body`
226
238
  schema = self.definition["schema"]
227
- return self.transform_keywords(schema)
239
+ return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
228
240
 
229
241
 
230
242
  FORM_MEDIA_TYPES = ("multipart/form-data", "application/x-www-form-urlencoded")
@@ -243,13 +255,13 @@ class OpenAPI30Body(OpenAPIBody, OpenAPI30Parameter):
243
255
  required: bool = False
244
256
  description: str | None = None
245
257
 
246
- def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
258
+ def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
247
259
  """Convert body definition to JSON Schema."""
248
260
  schema = get_media_type_schema(self.definition)
249
- return self.transform_keywords(schema)
261
+ return self.transform_keywords(schema, update_quantifiers=update_quantifiers)
250
262
 
251
- def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
252
- definition = super().transform_keywords(schema)
263
+ def transform_keywords(self, schema: dict[str, Any], *, update_quantifiers: bool = True) -> dict[str, Any]:
264
+ definition = super().transform_keywords(schema, update_quantifiers=update_quantifiers)
253
265
  if self.is_form:
254
266
  # It significantly reduces the "filtering" part of data generation.
255
267
  definition.setdefault("type", "object")
@@ -287,12 +299,14 @@ class OpenAPI20CompositeBody(OpenAPIBody, OpenAPI20Parameter):
287
299
  # We generate an object for formData - it is always required.
288
300
  return bool(self.definition)
289
301
 
290
- def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
302
+ def as_json_schema(self, operation: APIOperation, *, update_quantifiers: bool = True) -> dict[str, Any]:
291
303
  """The composite body is transformed into an "object" JSON Schema."""
292
- return parameters_to_json_schema(operation, self.definition)
304
+ return parameters_to_json_schema(operation, self.definition, update_quantifiers=update_quantifiers)
293
305
 
294
306
 
295
- def parameters_to_json_schema(operation: APIOperation, parameters: Iterable[OpenAPIParameter]) -> dict[str, Any]:
307
+ def parameters_to_json_schema(
308
+ operation: APIOperation, parameters: Iterable[OpenAPIParameter], *, update_quantifiers: bool = True
309
+ ) -> dict[str, Any]:
296
310
  """Create an "object" JSON schema from a list of Open API parameters.
297
311
 
298
312
  :param List[OpenAPIParameter] parameters: A list of Open API parameters related to the same location. All of
@@ -332,7 +346,7 @@ def parameters_to_json_schema(operation: APIOperation, parameters: Iterable[Open
332
346
  required = []
333
347
  for parameter in parameters:
334
348
  name = parameter.name
335
- properties[name] = parameter.as_json_schema(operation)
349
+ properties[name] = parameter.as_json_schema(operation, update_quantifiers=update_quantifiers)
336
350
  # If parameter names are duplicated, we need to avoid duplicate entries in `required` anyway
337
351
  if parameter.is_required and name not in required:
338
352
  required.append(name)