schemathesis 3.30.4__py3-none-any.whl → 3.31.1__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.
@@ -522,6 +522,13 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
522
522
  type=click.File("w", encoding="utf-8"),
523
523
  is_eager=True,
524
524
  )
525
+ @click.option(
526
+ "--cassette-format",
527
+ help="Format of the saved cassettes.",
528
+ type=click.Choice([item.name.lower() for item in cassettes.CassetteFormat]),
529
+ default=cassettes.CassetteFormat.VCR.name.lower(),
530
+ callback=callbacks.convert_cassette_format,
531
+ )
525
532
  @click.option(
526
533
  "--cassette-preserve-exact-body-bytes",
527
534
  help="Retains exact byte sequence of payloads in cassettes, encoded as base64.",
@@ -718,6 +725,14 @@ The report data, consisting of a tar gz file with multiple JSON files, is subjec
718
725
  default="utf-8",
719
726
  callback=callbacks.validate_generation_codec,
720
727
  )
728
+ @click.option(
729
+ "--generation-with-security-parameters",
730
+ help="Whether to generate security parameters.",
731
+ type=str,
732
+ default="true",
733
+ show_default=True,
734
+ callback=callbacks.convert_boolean_string,
735
+ )
721
736
  @click.option(
722
737
  "--schemathesis-io-token",
723
738
  help="Schemathesis.io authentication token.",
@@ -783,6 +798,7 @@ def run(
783
798
  show_trace: bool = False,
784
799
  code_sample_style: CodeSampleStyle = CodeSampleStyle.default(),
785
800
  cassette_path: click.utils.LazyFile | None = None,
801
+ cassette_format: cassettes.CassetteFormat = cassettes.CassetteFormat.VCR,
786
802
  cassette_preserve_exact_body_bytes: bool = False,
787
803
  store_network_log: click.utils.LazyFile | None = None,
788
804
  wait_for_schema: float | None = None,
@@ -810,6 +826,7 @@ def run(
810
826
  no_color: bool = False,
811
827
  report_value: str | None = None,
812
828
  generation_allow_x00: bool = True,
829
+ generation_with_security_parameters: bool = True,
813
830
  generation_codec: str = "utf-8",
814
831
  schemathesis_io_token: str | None = None,
815
832
  schemathesis_io_url: str = service.DEFAULT_URL,
@@ -849,7 +866,11 @@ def run(
849
866
 
850
867
  override = CaseOverride(query=set_query, headers=set_header, cookies=set_cookie, path_parameters=set_path)
851
868
 
852
- generation_config = generation.GenerationConfig(allow_x00=generation_allow_x00, codec=generation_codec)
869
+ generation_config = generation.GenerationConfig(
870
+ allow_x00=generation_allow_x00,
871
+ codec=generation_codec,
872
+ with_security_parameters=generation_with_security_parameters,
873
+ )
853
874
 
854
875
  report: ReportToService | click.utils.LazyFile | None
855
876
  if report_value is None:
@@ -1006,6 +1027,7 @@ def run(
1006
1027
  wait_for_schema=wait_for_schema,
1007
1028
  validate_schema=validate_schema,
1008
1029
  cassette_path=cassette_path,
1030
+ cassette_format=cassette_format,
1009
1031
  cassette_preserve_exact_body_bytes=cassette_preserve_exact_body_bytes,
1010
1032
  junit_xml=junit_xml,
1011
1033
  verbosity=verbosity,
@@ -1387,6 +1409,7 @@ def execute(
1387
1409
  wait_for_schema: float | None,
1388
1410
  validate_schema: bool,
1389
1411
  cassette_path: click.utils.LazyFile | None,
1412
+ cassette_format: cassettes.CassetteFormat,
1390
1413
  cassette_preserve_exact_body_bytes: bool,
1391
1414
  junit_xml: click.utils.LazyFile | None,
1392
1415
  verbosity: int,
@@ -1449,7 +1472,9 @@ def execute(
1449
1472
  # This handler should be first to have logs writing completed when the output handler will display statistic
1450
1473
  _open_file(cassette_path)
1451
1474
  handlers.append(
1452
- cassettes.CassetteWriter(cassette_path, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes)
1475
+ cassettes.CassetteWriter(
1476
+ cassette_path, format=cassette_format, preserve_exact_body_bytes=cassette_preserve_exact_body_bytes
1477
+ )
1453
1478
  )
1454
1479
  handlers.append(get_output_handler(workers_num))
1455
1480
  if sanitize_output:
@@ -24,6 +24,7 @@ from ..service.hosts import get_temporary_hosts_file
24
24
  from ..stateful import Stateful
25
25
  from ..transports.headers import has_invalid_characters, is_latin_1_encodable
26
26
  from ..types import PathLike
27
+ from .cassettes import CassetteFormat
27
28
  from .constants import DEFAULT_WORKERS
28
29
 
29
30
  if TYPE_CHECKING:
@@ -344,6 +345,10 @@ def convert_code_sample_style(ctx: click.core.Context, param: click.core.Paramet
344
345
  return CodeSampleStyle.from_str(value)
345
346
 
346
347
 
348
+ def convert_cassette_format(ctx: click.core.Context, param: click.core.Parameter, value: str) -> CassetteFormat:
349
+ return CassetteFormat.from_str(value)
350
+
351
+
347
352
  def convert_data_generation_method(
348
353
  ctx: click.core.Context, param: click.core.Parameter, value: str
349
354
  ) -> list[DataGenerationMethod]:
@@ -1,13 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import base64
4
+ import enum
4
5
  import json
5
6
  import re
6
7
  import sys
7
8
  import threading
8
9
  from dataclasses import dataclass, field
10
+ from http.cookies import SimpleCookie
9
11
  from queue import Queue
10
- from typing import IO, TYPE_CHECKING, Any, Generator, Iterator, cast
12
+ from typing import IO, TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
13
+ from urllib.parse import parse_qsl, urlparse
14
+
15
+ import harfile
11
16
 
12
17
  from ..constants import SCHEMATHESIS_VERSION
13
18
  from ..runner import events
@@ -27,6 +32,23 @@ if TYPE_CHECKING:
27
32
  WRITER_WORKER_JOIN_TIMEOUT = 1
28
33
 
29
34
 
35
+ class CassetteFormat(str, enum.Enum):
36
+ """Type of the cassette."""
37
+
38
+ VCR = "vcr"
39
+ HAR = "har"
40
+
41
+ @classmethod
42
+ def from_str(cls, value: str) -> CassetteFormat:
43
+ try:
44
+ return cls[value.upper()]
45
+ except KeyError:
46
+ available_formats = ", ".join(cls)
47
+ raise ValueError(
48
+ f"Invalid value for cassette format: {value}. Available formats: {available_formats}"
49
+ ) from None
50
+
51
+
30
52
  @dataclass
31
53
  class CassetteWriter(EventHandler):
32
54
  """Write interactions in a YAML cassette.
@@ -36,26 +58,30 @@ class CassetteWriter(EventHandler):
36
58
  """
37
59
 
38
60
  file_handle: click.utils.LazyFile
61
+ format: CassetteFormat
39
62
  preserve_exact_body_bytes: bool
40
63
  queue: Queue = field(default_factory=Queue)
41
64
  worker: threading.Thread = field(init=False)
42
65
 
43
66
  def __post_init__(self) -> None:
44
- self.worker = threading.Thread(
45
- target=worker,
46
- kwargs={
47
- "file_handle": self.file_handle,
48
- "preserve_exact_body_bytes": self.preserve_exact_body_bytes,
49
- "queue": self.queue,
50
- },
51
- )
67
+ kwargs = {
68
+ "file_handle": self.file_handle,
69
+ "queue": self.queue,
70
+ "preserve_exact_body_bytes": self.preserve_exact_body_bytes,
71
+ }
72
+ writer: Callable
73
+ if self.format == CassetteFormat.HAR:
74
+ writer = har_writer
75
+ else:
76
+ writer = vcr_writer
77
+ self.worker = threading.Thread(name="SchemathesisCassetteWriter", target=writer, kwargs=kwargs)
52
78
  self.worker.start()
53
79
 
54
80
  def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
55
81
  if isinstance(event, events.Initialized):
56
82
  # In the beginning we write metadata and start `http_interactions` list
57
83
  self.queue.put(Initialize())
58
- if isinstance(event, events.AfterExecution):
84
+ elif isinstance(event, events.AfterExecution):
59
85
  # Seed is always present at this point, the original Optional[int] type is there because `TestResult`
60
86
  # instance is created before `seed` is generated on the hypothesis side
61
87
  seed = cast(int, event.result.seed)
@@ -71,7 +97,19 @@ class CassetteWriter(EventHandler):
71
97
  interactions=event.result.interactions,
72
98
  )
73
99
  )
74
- if isinstance(event, events.Finished):
100
+ elif isinstance(event, events.AfterStatefulExecution):
101
+ seed = cast(int, event.result.seed)
102
+ self.queue.put(
103
+ Process(
104
+ seed=seed,
105
+ # Correlation ID is not used in stateful testing
106
+ correlation_id="",
107
+ thread_id=event.thread_id,
108
+ data_generation_method=event.data_generation_method[0],
109
+ interactions=event.result.interactions,
110
+ )
111
+ )
112
+ elif isinstance(event, events.Finished):
75
113
  self.shutdown()
76
114
 
77
115
  def shutdown(self) -> None:
@@ -112,7 +150,7 @@ def get_command_representation() -> str:
112
150
  return f"st {args}"
113
151
 
114
152
 
115
- def worker(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
153
+ def vcr_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
116
154
  """Write YAML to a file in an incremental manner.
117
155
 
118
156
  This implementation doesn't use `pyyaml` package and composes YAML manually as string due to the following reasons:
@@ -278,6 +316,106 @@ def write_double_quoted(stream: IO, text: str) -> None:
278
316
  stream.write('"')
279
317
 
280
318
 
319
+ def har_writer(file_handle: click.utils.LazyFile, preserve_exact_body_bytes: bool, queue: Queue) -> None:
320
+ if preserve_exact_body_bytes:
321
+
322
+ def get_body(body: str) -> str:
323
+ return body
324
+ else:
325
+
326
+ def get_body(body: str) -> str:
327
+ return base64.b64decode(body).decode("utf-8", errors="replace")
328
+
329
+ with harfile.open(file_handle) as har:
330
+ while True:
331
+ item = queue.get()
332
+ if isinstance(item, Process):
333
+ for interaction in item.interactions:
334
+ time = round(interaction.response.elapsed * 1000, 2)
335
+ content_type = interaction.response.headers.get("Content-Type", [""])[0]
336
+ content = harfile.Content(
337
+ size=interaction.response.body_size or 0,
338
+ mimeType=content_type,
339
+ text=get_body(interaction.response.body) if interaction.response.body is not None else None,
340
+ encoding="base64"
341
+ if interaction.response.body is not None and preserve_exact_body_bytes
342
+ else None,
343
+ )
344
+ http_version = f"HTTP/{interaction.response.http_version}"
345
+ query_params = urlparse(interaction.request.uri).query
346
+ if interaction.request.body is not None:
347
+ post_data = harfile.PostData(
348
+ mimeType=content_type,
349
+ text=get_body(interaction.request.body),
350
+ )
351
+ else:
352
+ post_data = None
353
+ har.add_entry(
354
+ startedDateTime=interaction.recorded_at,
355
+ time=time,
356
+ request=harfile.Request(
357
+ method=interaction.request.method.upper(),
358
+ url=interaction.request.uri,
359
+ httpVersion=http_version,
360
+ headers=[
361
+ harfile.Record(name=name, value=values[0])
362
+ for name, values in interaction.request.headers.items()
363
+ ],
364
+ queryString=[
365
+ harfile.Record(name=name, value=value)
366
+ for name, value in parse_qsl(query_params, keep_blank_values=True)
367
+ ],
368
+ cookies=_extract_cookies(interaction.request.headers.get("Cookie", [])),
369
+ headersSize=_headers_size(interaction.request.headers),
370
+ bodySize=interaction.request.body_size or 0,
371
+ postData=post_data,
372
+ ),
373
+ response=harfile.Response(
374
+ status=interaction.response.status_code,
375
+ httpVersion=http_version,
376
+ statusText=interaction.response.message,
377
+ headers=[
378
+ harfile.Record(name=name, value=values[0])
379
+ for name, values in interaction.response.headers.items()
380
+ ],
381
+ cookies=_extract_cookies(interaction.response.headers.get("Set-Cookie", [])),
382
+ content=content,
383
+ headersSize=_headers_size(interaction.response.headers),
384
+ bodySize=interaction.response.body_size or 0,
385
+ redirectURL=interaction.response.headers.get("Location", [""])[0],
386
+ ),
387
+ timings=harfile.Timings(send=0, wait=0, receive=time, blocked=0, dns=0, connect=0, ssl=0),
388
+ )
389
+ elif isinstance(item, Finalize):
390
+ break
391
+
392
+
393
+ def _headers_size(headers: dict[str, list[str]]) -> int:
394
+ size = 0
395
+ for name, values in headers.items():
396
+ # 4 is for ": " and "\r\n"
397
+ size += len(name) + 4 + len(values[0])
398
+ return size
399
+
400
+
401
+ def _extract_cookies(headers: list[str]) -> list[harfile.Cookie]:
402
+ return [cookie for items in headers for item in items for cookie in _cookie_to_har(item)]
403
+
404
+
405
+ def _cookie_to_har(cookie: str) -> Iterator[harfile.Cookie]:
406
+ parsed = SimpleCookie(cookie)
407
+ for name, data in parsed.items():
408
+ yield harfile.Cookie(
409
+ name=name,
410
+ value=data.value,
411
+ path=data["path"] or None,
412
+ domain=data["domain"] or None,
413
+ expires=data["expires"] or None,
414
+ httpOnly=data["httponly"] or None,
415
+ secure=data["secure"] or None,
416
+ )
417
+
418
+
281
419
  @dataclass
282
420
  class Replayed:
283
421
  interaction: dict[str, Any]
@@ -222,9 +222,11 @@ class OperationSchemaError(Exception):
222
222
  def from_reference_resolution_error(
223
223
  cls, error: RefResolutionError, path: str | None, method: str | None, full_path: str | None
224
224
  ) -> OperationSchemaError:
225
+ notes = getattr(error, "__notes__", [])
226
+ # Some exceptions don't have the actual reference in them, hence we add it manually via notes
227
+ pointer = f"'{notes[0]}'"
225
228
  message = "Unresolvable JSON pointer in the schema"
226
229
  # Get the pointer value from "Unresolvable JSON pointer: 'components/UnknownParameter'"
227
- pointer = str(error).split(": ", 1)[-1]
228
230
  message += f"\n\nError details:\n JSON pointer: {pointer}"
229
231
  message += "\n This typically means that the schema is referencing a component that doesn't exist."
230
232
  message += f"\n\n{SCHEMA_ERROR_SUGGESTION}"
@@ -77,5 +77,7 @@ class GenerationConfig:
77
77
  allow_x00: bool = True
78
78
  # Generate strings using the given codec
79
79
  codec: str | None = "utf-8"
80
+ # Whether to generate security parameters
81
+ with_security_parameters: bool = True
80
82
  # Header generation configuration
81
83
  headers: HeaderConfig = field(default_factory=HeaderConfig)
schemathesis/models.py CHANGED
@@ -119,6 +119,19 @@ def prepare_request_data(kwargs: dict[str, Any]) -> PreparedRequestData:
119
119
  )
120
120
 
121
121
 
122
+ @dataclass
123
+ class GenerationMetadata:
124
+ """Stores various information about how data is generated."""
125
+
126
+ query: DataGenerationMethod | None
127
+ path_parameters: DataGenerationMethod | None
128
+ headers: DataGenerationMethod | None
129
+ cookies: DataGenerationMethod | None
130
+ body: DataGenerationMethod | None
131
+
132
+ __slots__ = ("query", "path_parameters", "headers", "cookies", "body")
133
+
134
+
122
135
  @dataclass(repr=False)
123
136
  class Case:
124
137
  """A single test case parameters."""
@@ -139,6 +152,8 @@ class Case:
139
152
  media_type: str | None = None
140
153
  source: CaseSource | None = None
141
154
 
155
+ meta: GenerationMetadata | None = None
156
+
142
157
  # The way the case was generated (None for manually crafted ones)
143
158
  data_generation_method: DataGenerationMethod | None = None
144
159
  _auth: requests.auth.AuthBase | None = None
@@ -492,6 +507,7 @@ class Case:
492
507
  query=fast_deepcopy(self.query),
493
508
  body=fast_deepcopy(self.body),
494
509
  generation_time=self.generation_time,
510
+ id=self.id,
495
511
  )
496
512
 
497
513
 
@@ -831,6 +847,7 @@ class Request:
831
847
  method: str
832
848
  uri: str
833
849
  body: str | None
850
+ body_size: int | None
834
851
  headers: Headers
835
852
 
836
853
  @classmethod
@@ -861,6 +878,7 @@ class Request:
861
878
  method=method,
862
879
  headers={key: [value] for (key, value) in prepared.headers.items()},
863
880
  body=serialize_payload(body) if body is not None else body,
881
+ body_size=len(body) if body is not None else None,
864
882
  )
865
883
 
866
884
  def deserialize_body(self) -> bytes | None:
@@ -880,6 +898,7 @@ class Response:
880
898
  message: str
881
899
  headers: dict[str, list[str]]
882
900
  body: str | None
901
+ body_size: int | None
883
902
  encoding: str | None
884
903
  http_version: str
885
904
  elapsed: float
@@ -906,6 +925,7 @@ class Response:
906
925
  status_code=response.status_code,
907
926
  message=response.reason,
908
927
  body=body,
928
+ body_size=len(response.content) if body is not None else None,
909
929
  encoding=response.encoding,
910
930
  headers=headers,
911
931
  http_version=http_version,
@@ -933,6 +953,7 @@ class Response:
933
953
  status_code=response.status_code,
934
954
  message=message,
935
955
  body=body,
956
+ body_size=len(data) if body is not None else None,
936
957
  encoding=encoding,
937
958
  headers=headers,
938
959
  http_version="1.1",
@@ -949,6 +970,9 @@ class Response:
949
970
  return deserialize_payload(self.body)
950
971
 
951
972
 
973
+ TIMEZONE = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
974
+
975
+
952
976
  @dataclass
953
977
  class Interaction:
954
978
  """A single interaction with the target app."""
@@ -958,7 +982,7 @@ class Interaction:
958
982
  checks: list[Check]
959
983
  status: Status
960
984
  data_generation_method: DataGenerationMethod
961
- recorded_at: str = field(default_factory=lambda: datetime.datetime.now().isoformat())
985
+ recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
962
986
 
963
987
  @classmethod
964
988
  def from_requests(cls, case: Case, response: requests.Response, status: Status, checks: list[Check]) -> Interaction:
@@ -303,6 +303,8 @@ class AfterStatefulExecution(ExecutionEvent):
303
303
 
304
304
  status: Status
305
305
  result: SerializedTestResult
306
+ data_generation_method: list[DataGenerationMethod]
307
+ thread_id: int = field(default_factory=threading.get_ident)
306
308
 
307
309
 
308
310
  @dataclass
@@ -236,12 +236,54 @@ class BaseRunner:
236
236
  state_machine = self.schema.as_state_machine()
237
237
  runner = state_machine.runner(config=config)
238
238
  status = Status.success
239
+
240
+ def from_step_status(step_status: stateful_events.StepStatus) -> Status:
241
+ return {
242
+ stateful_events.StepStatus.SUCCESS: Status.success,
243
+ stateful_events.StepStatus.FAILURE: Status.failure,
244
+ stateful_events.StepStatus.ERROR: Status.error,
245
+ stateful_events.StepStatus.INTERRUPTED: Status.error,
246
+ }[step_status]
247
+
248
+ if self.store_interactions:
249
+ if isinstance(state_machine.schema.transport, RequestsTransport):
250
+
251
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
252
+ if event.response is not None:
253
+ response = cast(requests.Response, event.response)
254
+ result.store_requests_response(
255
+ status=from_step_status(event.status),
256
+ case=event.case,
257
+ response=response,
258
+ checks=event.checks,
259
+ )
260
+
261
+ else:
262
+
263
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
264
+ if event.response is not None:
265
+ response = cast(WSGIResponse, event.response)
266
+ result.store_wsgi_response(
267
+ status=from_step_status(event.status),
268
+ case=event.case,
269
+ response=response,
270
+ headers=self.headers or {},
271
+ elapsed=response.elapsed.total_seconds(),
272
+ checks=event.checks,
273
+ )
274
+ else:
275
+
276
+ def on_step_finished(event: stateful_events.StepFinished) -> None:
277
+ return None
278
+
239
279
  for stateful_event in runner.execute():
240
280
  if isinstance(stateful_event, stateful_events.SuiteFinished):
241
281
  if stateful_event.failures and status != Status.error:
242
282
  status = Status.failure
243
283
  for failure in stateful_event.failures:
244
284
  result.checks.append(failure)
285
+ elif isinstance(stateful_event, stateful_events.StepFinished):
286
+ on_step_finished(stateful_event)
245
287
  elif isinstance(stateful_event, stateful_events.Errored):
246
288
  status = Status.error
247
289
  yield events.StatefulEvent(data=stateful_event)
@@ -249,6 +291,7 @@ class BaseRunner:
249
291
  yield events.AfterStatefulExecution(
250
292
  status=status,
251
293
  result=SerializedTestResult.from_test_result(result),
294
+ data_generation_method=self.schema.data_generation_methods,
252
295
  )
253
296
 
254
297
  def _run_tests(
@@ -171,13 +171,36 @@ def _serialize_stateful_event(event: stateful_events.StatefulEvent) -> dict[str,
171
171
  "timestamp": event.timestamp,
172
172
  "exception": format_exception(event.exception, True),
173
173
  }
174
+ elif isinstance(event, stateful_events.StepFinished):
175
+ data = {
176
+ "timestamp": event.timestamp,
177
+ "status": event.status,
178
+ "transition_id": {
179
+ "name": event.transition_id.name,
180
+ "status_code": event.transition_id.status_code,
181
+ "source": event.transition_id.source,
182
+ }
183
+ if event.transition_id is not None
184
+ else None,
185
+ "target": event.target,
186
+ "response": {
187
+ "status_code": event.response.status_code,
188
+ "elapsed": event.response.elapsed.total_seconds(),
189
+ }
190
+ if event.response is not None
191
+ else None,
192
+ }
174
193
  else:
175
194
  data = asdict(event)
176
195
  return {"data": {event.__class__.__name__: data}}
177
196
 
178
197
 
179
198
  def serialize_after_stateful_execution(event: events.AfterStatefulExecution) -> dict[str, Any] | None:
180
- return {"result": asdict(event.result)}
199
+ return {
200
+ "status": event.status,
201
+ "data_generation_method": event.data_generation_method,
202
+ "result": asdict(event.result),
203
+ }
181
204
 
182
205
 
183
206
  SERIALIZER_MAP = {
@@ -25,7 +25,7 @@ from ...generation import DataGenerationMethod, GenerationConfig
25
25
  from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
26
26
  from ...internal.copy import fast_deepcopy
27
27
  from ...internal.validation import is_illegal_surrogate
28
- from ...models import APIOperation, Case, cant_serialize
28
+ from ...models import APIOperation, Case, GenerationMetadata, cant_serialize
29
29
  from ...serializers import Binary
30
30
  from ...transports.content_types import parse_content_type
31
31
  from ...transports.headers import has_invalid_characters, is_latin_1_encodable
@@ -36,7 +36,7 @@ from .formats import STRING_FORMATS
36
36
  from .media_types import MEDIA_TYPES
37
37
  from .negative import negative_schema
38
38
  from .negative.utils import can_negate
39
- from .parameters import OpenAPIBody, parameters_to_json_schema
39
+ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
40
40
  from .utils import is_header_location
41
41
 
42
42
  HEADER_FORMAT = "_header_value"
@@ -207,6 +207,13 @@ def get_case_strategy(
207
207
  query=query_.value,
208
208
  body=body_.value,
209
209
  data_generation_method=generator,
210
+ meta=GenerationMetadata(
211
+ query=query_.generator,
212
+ path_parameters=path_parameters_.generator,
213
+ headers=headers_.generator,
214
+ cookies=cookies_.generator,
215
+ body=body_.generator,
216
+ ),
210
217
  )
211
218
  auth_context = auths.AuthContext(
212
219
  operation=operation,
@@ -347,6 +354,22 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
347
354
  return any(header != {"type": "string"} for header in headers.values())
348
355
 
349
356
 
357
+ def get_schema_for_location(
358
+ operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
359
+ ) -> dict[str, Any]:
360
+ schema = parameters_to_json_schema(operation, parameters)
361
+ if location == "path":
362
+ if not operation.schema.validate_schema:
363
+ # If schema validation is disabled, we try to generate data even if the parameter definition
364
+ # contains errors.
365
+ # In this case, we know that the `required` keyword should always be `True`.
366
+ schema["required"] = list(schema["properties"])
367
+ for prop in schema.get("properties", {}).values():
368
+ if prop.get("type") == "string":
369
+ prop.setdefault("minLength", 1)
370
+ return operation.schema.prepare_schema(schema)
371
+
372
+
350
373
  def get_parameters_strategy(
351
374
  operation: APIOperation,
352
375
  strategy_factory: StrategyFactory,
@@ -361,17 +384,7 @@ def get_parameters_strategy(
361
384
  nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
362
385
  if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
363
386
  return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
364
- schema = parameters_to_json_schema(operation, parameters)
365
- if location == "path":
366
- if not operation.schema.validate_schema:
367
- # If schema validation is disabled, we try to generate data even if the parameter definition
368
- # contains errors.
369
- # In this case, we know that the `required` keyword should always be `True`.
370
- schema["required"] = list(schema["properties"])
371
- for prop in schema.get("properties", {}).values():
372
- if prop.get("type") == "string":
373
- prop.setdefault("minLength", 1)
374
- schema = operation.schema.prepare_schema(schema)
387
+ schema = get_schema_for_location(operation, location, parameters)
375
388
  for name in exclude:
376
389
  # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
377
390
  # that may be invalid
@@ -145,7 +145,12 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
145
145
 
146
146
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
147
147
  return True
148
- if case.data_generation_method and case.data_generation_method.is_negative and 200 <= response.status_code < 300:
148
+ if (
149
+ case.data_generation_method
150
+ and case.data_generation_method.is_negative
151
+ and 200 <= response.status_code < 300
152
+ and not has_only_additional_properties_in_non_body_parameters(case)
153
+ ):
149
154
  exc_class = get_negative_rejection_error(case.operation.verbose_name, response.status_code)
150
155
  raise exc_class(
151
156
  failures.AcceptedNegativeData.title,
@@ -154,6 +159,34 @@ def negative_data_rejection(response: GenericResponse, case: Case) -> bool | Non
154
159
  return None
155
160
 
156
161
 
162
+ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
163
+ # Check if the case contains only additional properties in query, headers, or cookies.
164
+ # This function is used to determine if negation is solely in the form of extra properties,
165
+ # which are often ignored for backward-compatibility by the tested apps
166
+ from ._hypothesis import get_schema_for_location
167
+
168
+ meta = case.meta
169
+ if meta is None:
170
+ # Ignore manually created cases
171
+ return False
172
+ if (meta.body and meta.body.is_negative) or (meta.path_parameters and meta.path_parameters.is_negative):
173
+ # Body or path negations always imply other negations
174
+ return False
175
+ validator_cls = case.operation.schema.validator_cls # type: ignore[attr-defined]
176
+ for container in ("query", "headers", "cookies"):
177
+ meta_for_location = getattr(meta, container)
178
+ value = getattr(case, container)
179
+ if value is not None and meta_for_location is not None and meta_for_location.is_negative:
180
+ parameters = getattr(case.operation, container)
181
+ value_without_additional_properties = {k: v for k, v in value.items() if k in parameters}
182
+ schema = get_schema_for_location(case.operation, container, parameters)
183
+ if not validator_cls(schema).is_valid(value_without_additional_properties):
184
+ # Other types of negation found
185
+ return False
186
+ # Only additional properties are added
187
+ return True
188
+
189
+
157
190
  def use_after_free(response: GenericResponse, original: Case) -> bool | None:
158
191
  from ...transports.responses import get_reason
159
192
  from .schemas import BaseOpenAPISchema
@@ -81,6 +81,10 @@ class MutationContext:
81
81
  def is_path_location(self) -> bool:
82
82
  return self.location == "path"
83
83
 
84
+ @property
85
+ def is_query_location(self) -> bool:
86
+ return self.location == "query"
87
+
84
88
  def mutate(self, draw: Draw) -> Schema:
85
89
  # On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
86
90
  # taken as-is. Therefore, we can only apply mutations that won't change the Open API semantics of the schema.
@@ -203,8 +207,11 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
203
207
  if context.media_type == "application/x-www-form-urlencoded":
204
208
  # Form data should be an object, do not change it
205
209
  return MutationResult.FAILURE
206
- # Headers are always strings, can't negate this
207
- 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):
208
215
  return MutationResult.FAILURE
209
216
  candidates = _get_type_candidates(context, schema)
210
217
  if not candidates:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from dataclasses import dataclass
4
5
  from functools import lru_cache
5
6
  from typing import Any, Callable, Dict, Union, overload
@@ -7,6 +8,7 @@ from urllib.request import urlopen
7
8
 
8
9
  import jsonschema
9
10
  import requests
11
+ from jsonschema.exceptions import RefResolutionError
10
12
 
11
13
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
12
14
  from ...internal.copy import fast_deepcopy
@@ -55,6 +57,23 @@ class InliningResolver(jsonschema.RefResolver):
55
57
  )
56
58
  super().__init__(*args, **kwargs)
57
59
 
60
+ if sys.version_info >= (3, 11):
61
+
62
+ def resolve(self, ref: str) -> tuple[str, Any]:
63
+ try:
64
+ return super().resolve(ref)
65
+ except RefResolutionError as exc:
66
+ exc.add_note(ref)
67
+ raise
68
+ else:
69
+
70
+ def resolve(self, ref: str) -> tuple[str, Any]:
71
+ try:
72
+ return super().resolve(ref)
73
+ except RefResolutionError as exc:
74
+ exc.__notes__ = [ref]
75
+ raise
76
+
58
77
  @overload
59
78
  def resolve_all(self, item: dict[str, Any], recursion_level: int = 0) -> dict[str, Any]:
60
79
  pass
@@ -19,6 +19,7 @@ from typing import (
19
19
  Mapping,
20
20
  NoReturn,
21
21
  Sequence,
22
+ Type,
22
23
  TypeVar,
23
24
  cast,
24
25
  )
@@ -77,7 +78,13 @@ from .parameters import (
77
78
  OpenAPI30Parameter,
78
79
  OpenAPIParameter,
79
80
  )
80
- from .references import RECURSION_DEPTH_LIMIT, UNRESOLVABLE, ConvertingResolver, InliningResolver, resolve_pointer
81
+ from .references import (
82
+ RECURSION_DEPTH_LIMIT,
83
+ UNRESOLVABLE,
84
+ ConvertingResolver,
85
+ InliningResolver,
86
+ resolve_pointer,
87
+ )
81
88
  from .security import BaseSecurityProcessor, OpenAPISecurityProcessor, SwaggerSecurityProcessor
82
89
  from .stateful import create_state_machine
83
90
 
@@ -385,7 +392,8 @@ class BaseOpenAPISchema(BaseSchema):
385
392
  )
386
393
  for parameter in parameters:
387
394
  operation.add_parameter(parameter)
388
- self.security.process_definitions(self.raw_schema, operation, self.resolver)
395
+ if self.generation_config.with_security_parameters:
396
+ self.security.process_definitions(self.raw_schema, operation, self.resolver)
389
397
  self.dispatch_hook("before_init_operation", HookContext(operation=operation), operation)
390
398
  return operation
391
399
 
@@ -507,12 +515,13 @@ class BaseOpenAPISchema(BaseSchema):
507
515
 
508
516
  def get_parameter_serializer(self, operation: APIOperation, location: str) -> Callable | None:
509
517
  definitions = [item.definition for item in operation.iter_parameters() if item.location == location]
510
- security_parameters = self.security.get_security_definitions_as_parameters(
511
- self.raw_schema, operation, self.resolver, location
512
- )
513
- security_parameters = [item for item in security_parameters if item["in"] == location]
514
- if security_parameters:
515
- definitions.extend(security_parameters)
518
+ if self.generation_config.with_security_parameters:
519
+ security_parameters = self.security.get_security_definitions_as_parameters(
520
+ self.raw_schema, operation, self.resolver, location
521
+ )
522
+ security_parameters = [item for item in security_parameters if item["in"] == location]
523
+ if security_parameters:
524
+ definitions.extend(security_parameters)
516
525
  if definitions:
517
526
  return self._get_parameter_serializer(definitions)
518
527
  return None
@@ -607,6 +616,12 @@ class BaseOpenAPISchema(BaseSchema):
607
616
  def get_tags(self, operation: APIOperation) -> list[str] | None:
608
617
  return operation.definition.raw.get("tags")
609
618
 
619
+ @property
620
+ def validator_cls(self) -> Type[jsonschema.Validator]:
621
+ if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
622
+ return jsonschema.Draft202012Validator
623
+ return jsonschema.Draft4Validator
624
+
610
625
  def validate_response(self, operation: APIOperation, response: GenericResponse) -> bool | None:
611
626
  responses = {str(key): value for key, value in operation.definition.raw.get("responses", {}).items()}
612
627
  status_code = str(response.status_code)
@@ -650,13 +665,9 @@ class BaseOpenAPISchema(BaseSchema):
650
665
  resolver = ConvertingResolver(
651
666
  self.location or "", self.raw_schema, nullable_name=self.nullable_name, is_response_schema=True
652
667
  )
653
- if self.spec_version.startswith("3.1") and experimental.OPEN_API_3_1.is_enabled:
654
- cls = jsonschema.Draft202012Validator
655
- else:
656
- cls = jsonschema.Draft4Validator
657
668
  with in_scopes(resolver, scopes):
658
669
  try:
659
- jsonschema.validate(data, schema, cls=cls, resolver=resolver)
670
+ jsonschema.validate(data, schema, cls=self.validator_cls, resolver=resolver)
660
671
  except jsonschema.ValidationError as exc:
661
672
  exc_class = get_schema_validation_error(operation.verbose_name, exc)
662
673
  ctx = failures.ValidationErrorContext.from_exception(exc, output_config=operation.schema.output_config)
@@ -126,10 +126,23 @@ class OpenAPISecurityProcessor(BaseSecurityProcessor):
126
126
  """In Open API 3 security definitions are located in ``components`` and may have references inside."""
127
127
  components = schema.get("components", {})
128
128
  security_schemes = components.get("securitySchemes", {})
129
+ # At this point, the resolution scope could differ from the root scope, that's why we need to restore it
130
+ # as now we resolve root-level references
131
+ if len(resolver._scopes_stack) > 1:
132
+ scope = resolver.resolution_scope
133
+ resolver.pop_scope()
134
+ else:
135
+ scope = None
129
136
  resolve = resolver.resolve
130
- if "$ref" in security_schemes:
131
- return resolve(security_schemes["$ref"])[1]
132
- return {key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()}
137
+ try:
138
+ if "$ref" in security_schemes:
139
+ return resolve(security_schemes["$ref"])[1]
140
+ return {
141
+ key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()
142
+ }
143
+ finally:
144
+ if scope is not None:
145
+ resolver._scopes_stack.append(scope)
133
146
 
134
147
  def _make_http_auth_parameter(self, definition: dict[str, Any]) -> dict[str, Any]:
135
148
  schema = make_auth_header_schema(definition)
@@ -39,6 +39,8 @@ class RunnerContext:
39
39
  seen_in_suite: set[FailureKey] = field(default_factory=set)
40
40
  # Unique failures collected in the current suite
41
41
  failures_for_suite: list[Check] = field(default_factory=list)
42
+ # All checks executed in the current run
43
+ checks_for_step: list[Check] = field(default_factory=list)
42
44
  # Status of the current step
43
45
  current_step_status: events.StepStatus | None = None
44
46
  current_response: GenericResponse | None = None
@@ -55,10 +57,13 @@ class RunnerContext:
55
57
  return events.ScenarioStatus.INTERRUPTED
56
58
  return events.ScenarioStatus.REJECTED
57
59
 
58
- def reset_step(self) -> None:
60
+ def reset_scenario(self) -> None:
59
61
  self.current_step_status = None
60
62
  self.current_response = None
61
63
 
64
+ def reset_step(self) -> None:
65
+ self.checks_for_step = []
66
+
62
67
  def step_succeeded(self) -> None:
63
68
  self.current_step_status = events.StepStatus.SUCCESS
64
69
 
@@ -100,4 +105,4 @@ class RunnerContext:
100
105
  def reset(self) -> None:
101
106
  self.failures_for_suite = []
102
107
  self.seen_in_suite.clear()
103
- self.reset_step()
108
+ self.reset_scenario()
@@ -6,7 +6,8 @@ from enum import Enum
6
6
  from typing import TYPE_CHECKING, Type
7
7
 
8
8
  if TYPE_CHECKING:
9
- from ..models import Check
9
+ from ..models import Case, Check
10
+ from ..transports.responses import GenericResponse
10
11
  from .state_machine import APIStateMachine
11
12
 
12
13
 
@@ -178,17 +179,28 @@ class StepFinished(StatefulEvent):
178
179
  status: StepStatus
179
180
  transition_id: TransitionId | None
180
181
  target: str
181
- response: ResponseData | None
182
+ case: Case
183
+ response: GenericResponse | None
184
+ checks: list[Check]
182
185
 
183
- __slots__ = ("timestamp", "status", "transition_id", "target", "response")
186
+ __slots__ = ("timestamp", "status", "transition_id", "target", "case", "response", "checks")
184
187
 
185
188
  def __init__(
186
- self, *, status: StepStatus, transition_id: TransitionId | None, target: str, response: ResponseData | None
189
+ self,
190
+ *,
191
+ status: StepStatus,
192
+ transition_id: TransitionId | None,
193
+ target: str,
194
+ case: Case,
195
+ response: GenericResponse | None,
196
+ checks: list[Check],
187
197
  ) -> None:
188
198
  self.status = status
189
199
  self.transition_id = transition_id
190
200
  self.target = target
201
+ self.case = case
191
202
  self.response = response
203
+ self.checks = checks
192
204
  self.timestamp = time.monotonic()
193
205
 
194
206
 
@@ -157,30 +157,25 @@ def _execute_state_machine_loop(
157
157
  )
158
158
  else:
159
159
  transition_id = None
160
- response: events.ResponseData | None
161
- if ctx.current_response is not None:
162
- response = events.ResponseData(
163
- status_code=ctx.current_response.status_code,
164
- elapsed=ctx.current_response.elapsed.total_seconds(),
165
- )
166
- else:
167
- response = None
168
160
  status = cast(events.StepStatus, ctx.current_step_status)
169
161
  event_queue.put(
170
162
  events.StepFinished(
171
163
  status=status,
172
164
  transition_id=transition_id,
173
165
  target=case.operation.verbose_name,
174
- response=response,
166
+ case=case,
167
+ response=ctx.current_response,
168
+ checks=ctx.checks_for_step,
175
169
  )
176
170
  )
171
+ ctx.reset_step()
177
172
  return result
178
173
 
179
174
  def validate_response(
180
175
  self, response: GenericResponse, case: Case, additional_checks: tuple[CheckFunction, ...] = ()
181
176
  ) -> None:
182
177
  ctx.current_response = response
183
- validate_response(response, case, ctx, config.checks, additional_checks)
178
+ validate_response(response, case, ctx, config.checks, ctx.checks_for_step, additional_checks)
184
179
 
185
180
  def teardown(self) -> None:
186
181
  build_ctx = current_build_context()
@@ -190,7 +185,7 @@ def _execute_state_machine_loop(
190
185
  is_final=build_ctx.is_final,
191
186
  )
192
187
  )
193
- ctx.reset_step()
188
+ ctx.reset_scenario()
194
189
  super().teardown()
195
190
 
196
191
  while True:
@@ -51,7 +51,7 @@ class StateMachineSink:
51
51
  responses = self.response_times.setdefault(event.target, {})
52
52
  if event.response is not None:
53
53
  average = responses.setdefault(event.response.status_code, AverageResponseTime())
54
- average.total += event.response.elapsed
54
+ average.total += event.response.elapsed.total_seconds()
55
55
  average.count += 1
56
56
  elif isinstance(event, events.ScenarioFinished):
57
57
  self.scenarios[event.status] += 1
@@ -7,7 +7,7 @@ from .context import RunnerContext
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from ..failures import FailureContext
10
- from ..models import Case, CheckFunction
10
+ from ..models import Case, CheckFunction, Check
11
11
  from ..transports.responses import GenericResponse
12
12
 
13
13
 
@@ -16,6 +16,7 @@ def validate_response(
16
16
  case: Case,
17
17
  failures: RunnerContext,
18
18
  checks: tuple[CheckFunction, ...],
19
+ check_results: list[Check],
19
20
  additional_checks: tuple[CheckFunction, ...] = (),
20
21
  ) -> None:
21
22
  """Validate the response against the provided checks."""
@@ -28,18 +29,18 @@ def validate_response(
28
29
  exceptions.append(exc)
29
30
  if failures.is_seen_in_suite(exc):
30
31
  return
31
- failures.add_failed_check(
32
- Check(
33
- name=name,
34
- value=Status.failure,
35
- response=response,
36
- elapsed=response.elapsed.total_seconds(),
37
- example=copied_case,
38
- message=message,
39
- context=context,
40
- request=None,
41
- )
32
+ failed_check = Check(
33
+ name=name,
34
+ value=Status.failure,
35
+ response=response,
36
+ elapsed=response.elapsed.total_seconds(),
37
+ example=copied_case,
38
+ message=message,
39
+ context=context,
40
+ request=None,
42
41
  )
42
+ failures.add_failed_check(failed_check)
43
+ check_results.append(failed_check)
43
44
  failures.mark_as_seen_in_suite(exc)
44
45
 
45
46
  for check in checks + additional_checks:
@@ -47,6 +48,17 @@ def validate_response(
47
48
  copied_case = case.partial_deepcopy()
48
49
  try:
49
50
  check(response, copied_case)
51
+ skip_check = check(response, copied_case)
52
+ if not skip_check:
53
+ passed_check = Check(
54
+ name=name,
55
+ value=Status.success,
56
+ response=response,
57
+ elapsed=response.elapsed.total_seconds(),
58
+ example=copied_case,
59
+ request=None,
60
+ )
61
+ check_results.append(passed_check)
50
62
  except CheckFailed as exc:
51
63
  if failures.is_seen_in_run(exc):
52
64
  continue
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: schemathesis
3
- Version: 3.30.4
3
+ Version: 3.31.1
4
4
  Summary: Property-based testing framework for Open API and GraphQL based apps
5
5
  Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
6
6
  Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
@@ -31,6 +31,7 @@ Requires-Python: >=3.8
31
31
  Requires-Dist: backoff<3.0,>=2.1.2
32
32
  Requires-Dist: click<9.0,>=7.0
33
33
  Requires-Dist: colorama<1.0,>=0.4
34
+ Requires-Dist: harfile<1.0,>=0.3.0
34
35
  Requires-Dist: httpx<1.0,>=0.22.0
35
36
  Requires-Dist: hypothesis-graphql<1,>=0.11.0
36
37
  Requires-Dist: hypothesis-jsonschema<0.24,>=0.23.1
@@ -10,14 +10,14 @@ schemathesis/auths.py,sha256=NeJqsjtDgJtHMyrHc6V1NTpkAh1K8ZKLalpB3v80cT4,14734
10
10
  schemathesis/checks.py,sha256=XplKduiRbRrxZx-wDMGaW91aIitr5RL9vjMP6bvCFRg,2272
11
11
  schemathesis/code_samples.py,sha256=xk1-1jnXg5hS40VzIZp8PEtZwGaazNlVKMT7_X-zG-M,4123
12
12
  schemathesis/constants.py,sha256=l1YQ7PXhEj9dyf9CTESVUpPOaFCH7iz-Fe8o4v6Th_s,2673
13
- schemathesis/exceptions.py,sha256=ZX_KqnkQ80GACsBgm6lJV43aFBJws-2vGtnFu72n-O4,19576
13
+ schemathesis/exceptions.py,sha256=tFDlNui1Sxc8a6-OQCIjj62SoToYoEN9zveguPpzQIc,19712
14
14
  schemathesis/failures.py,sha256=wXz5Kr5i-ojcYc-BdzFlNbNGOfoVXHZM6kd4iULdHK4,7003
15
15
  schemathesis/filters.py,sha256=0fYzn9sJ35k3Znx1P8FrbSdoUcdslcibtGh-IOTRwB8,10251
16
16
  schemathesis/graphql.py,sha256=YkoKWY5K8lxp7H3ikAs-IsoDbiPwJvChG7O8p3DgwtI,229
17
17
  schemathesis/hooks.py,sha256=dveqMmThIvt4fDahUXhU2nCq5pFvYjzzd1Ys_MhrJZA,12398
18
18
  schemathesis/lazy.py,sha256=eVdGkTZK0fWvUlFUCFGGlViH2NWEtYIjxiNkF4fBWhI,15218
19
19
  schemathesis/loaders.py,sha256=OtCD1o0TVmSNAUF7dgHpouoAXtY6w9vEtsRVGv4lE0g,4588
20
- schemathesis/models.py,sha256=4lICPGwcVHCICex774KFOFloqPdJWN6uI4Mq47bfSuw,44128
20
+ schemathesis/models.py,sha256=nbm9Agqw94RYib2Q4OH7iOiEPDGw64NlQnEB7o5Spio,44925
21
21
  schemathesis/parameters.py,sha256=PndmqQRlEYsCt1kWjSShPsFf6vj7X_7FRdz_-A95eNg,2258
22
22
  schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  schemathesis/sanitization.py,sha256=mRR4YvXpzqbmgX8Xu6rume6LBcz9g_oyusvbesZl44I,8958
@@ -27,10 +27,10 @@ schemathesis/targets.py,sha256=N1Zzgqa1PNycWIeGpra7q-6ASn2x4r9Jompn35bmlsE,1163
27
27
  schemathesis/throttling.py,sha256=aisUc4MJDGIOGUAs9L2DlWWpdd4KyAFuNVKhYoaUC9M,1719
28
28
  schemathesis/types.py,sha256=xOzNAeMs6qqeaJnWs5Fpw5JPbvVjyfRfxTJa3G2Ln5I,920
29
29
  schemathesis/utils.py,sha256=NX04p9mO-lCAH3DIISXDXPxWZk6lkGNM4-ubRi8vlvY,5234
30
- schemathesis/cli/__init__.py,sha256=EmgM2nTU81Q9BdAxwFl6hBsGZl1SYTr9tvCqHDyWu2U,65680
30
+ schemathesis/cli/__init__.py,sha256=Gw2uVput5JySsSg8gjAzibi-tK_PixwyNYyQLTukAJE,66535
31
31
  schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
32
- schemathesis/cli/callbacks.py,sha256=tZSe6EIyc1fdKZ8xGaE0w0LuGCMtT7m9cW41Rd0Nah0,14949
33
- schemathesis/cli/cassettes.py,sha256=WLt2svZm23vZX5buQWmWO94QwQEC8OM2g8yH50rfatE,12990
32
+ schemathesis/cli/callbacks.py,sha256=R2noVRu8zDBWmA3dJ2YnmhAjYdkxPDA1zIpA3_2DkFQ,15144
33
+ schemathesis/cli/cassettes.py,sha256=3vumBEP0XXBVpNvwMEGYvcIp1TSHtCNUIL60TPDVgD4,19024
34
34
  schemathesis/cli/constants.py,sha256=ogSuZs68KvzHNKF0yaiBkxLcGbo8IVR3xaIfsy1H1IQ,1546
35
35
  schemathesis/cli/context.py,sha256=EMeyAbU9mRujR46anc43yr6ab4rGYtIDaHC3cV9Qa-Q,2092
36
36
  schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
@@ -57,7 +57,7 @@ schemathesis/extra/pytest_plugin.py,sha256=ymicV2NjmSzee0ccUUUjNEvb9ihCxxf_8M60g
57
57
  schemathesis/fixups/__init__.py,sha256=RP5QYJVJhp8LXjhH89fCRaIVU26dHCy74jD9seoYMuc,967
58
58
  schemathesis/fixups/fast_api.py,sha256=mn-KzBqnR8jl4W5fY-_ZySabMDMUnpzCIESMHnlvE1c,1304
59
59
  schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZETA,745
60
- schemathesis/generation/__init__.py,sha256=sK7wYwW0s5fD4AtLft-_DWIlIqRDVb_cBxQK7tVYTi4,2278
60
+ schemathesis/generation/__init__.py,sha256=mC1NVAHyce1_B_wu-GYO9U21Gut8KFrjPXETREC9ABQ,2366
61
61
  schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
62
62
  schemathesis/internal/copy.py,sha256=lcK01CODz6jogXyH0tsKkPv1PBEX8jeBPxI9MzQ6LN4,942
63
63
  schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
@@ -68,11 +68,11 @@ schemathesis/internal/result.py,sha256=d449YvyONjqjDs-A5DAPgtAI96iT753K8sU6_1HLo
68
68
  schemathesis/internal/transformation.py,sha256=3S6AzAqdsEsB5iobFgSuvL0UMUqH0JHC7hGxKwcpqPw,450
69
69
  schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QAtMPyEw,966
70
70
  schemathesis/runner/__init__.py,sha256=n-4CanmlfMiGnir6xbZH-fM5a0pe2GloBFj2NTtet-M,21419
71
- schemathesis/runner/events.py,sha256=PJceb_jC-2BKEdMIrH8mSCtt8lbPPq6Gs5qjPicwxmU,10575
71
+ schemathesis/runner/events.py,sha256=VVFy-qAgxoHBNrexBakq_QX_hx52pnWK-67HgSPpYRw,10694
72
72
  schemathesis/runner/probes.py,sha256=J-TT0hOKu9j4htWKBcYKmsomcRxmvOl4WpmnKLVXu8M,5546
73
73
  schemathesis/runner/serialization.py,sha256=J8fuG8MSJq3rE3IJs73U1YXWFrNa05k7PGd5Bvq1uec,17356
74
74
  schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
75
- schemathesis/runner/impl/core.py,sha256=u56WvKOJY96ShHj5MoAcwys_th3DAxLAAoipx7wvWm8,42034
75
+ schemathesis/runner/impl/core.py,sha256=Ec_HlCRJdQSluN_JYEt0eh99SFXf0RGX6knPpayPUXw,44148
76
76
  schemathesis/runner/impl/solo.py,sha256=MatxThgqKsY2tX_hVwjy78oKFeKejb6dFJoX3kGzW4U,3359
77
77
  schemathesis/runner/impl/threadpool.py,sha256=fj2QYoWxIJIxpTCcJQyM_VCRO1YDnW9XQJJnNVFVQxY,15253
78
78
  schemathesis/service/__init__.py,sha256=cDVTCFD1G-vvhxZkJUwiToTAEQ-0ByIoqwXvJBCf_V8,472
@@ -86,7 +86,7 @@ schemathesis/service/hosts.py,sha256=ad2Lxq9Zcc9PP-1eFLQnxen4ImglcGOH8n7CGG72NNg
86
86
  schemathesis/service/metadata.py,sha256=x2LeCED1mdPf-YQJmjY8xtcIKHfD1ap5V0BGl-UgqNo,2087
87
87
  schemathesis/service/models.py,sha256=ihItUJ9CvH4TvmdfJY3W88NR82OODF8a3RD7WRXn6RM,6578
88
88
  schemathesis/service/report.py,sha256=4A8nf6_KOjDW3x1VXF8gSf_WY2xXp1Cbz-Owl_GeR7o,8294
89
- schemathesis/service/serialization.py,sha256=LbEUhcDI9hsFDrxjjH-79QhRd71pKHHc9odsVsUuC7s,9776
89
+ schemathesis/service/serialization.py,sha256=GFNc1-w1ShZopFdR7OFLT9ZKe8Y2S1hYnlXMrdMj3VE,10600
90
90
  schemathesis/service/usage.py,sha256=UbXqxeDq5mAjKkfV4hApZsReZmQHXiqoXUYn_Z6YuZk,2438
91
91
  schemathesis/specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
92
  schemathesis/specs/graphql/__init__.py,sha256=fgyHtvWNUVWismBTOqxQtgLoTighTfvMv6v6QCD_Oyc,85
@@ -98,8 +98,8 @@ schemathesis/specs/graphql/schemas.py,sha256=i6fAW9pYcOplQE7BejP6P8GQ9z6Y43Vx4_f
98
98
  schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
99
99
  schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
100
100
  schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
101
- schemathesis/specs/openapi/_hypothesis.py,sha256=9O8gTVWtq17UezgvlxHxjTHGVmggQ2mxwAnVIvZKgsk,23619
102
- schemathesis/specs/openapi/checks.py,sha256=1Fu3Kgai9ySCoGtCrx99Q9oVCEWXgkqHd1gTqG_569s,9364
101
+ schemathesis/specs/openapi/_hypothesis.py,sha256=hPctM9QN4mGZrEPnLbesoPEDrSF4TCyI5RMJUmchhnQ,24070
102
+ schemathesis/specs/openapi/checks.py,sha256=eFjbV9-0202qg0s0JjaBNRr7nf8Bd8VMLPHEfMvZoc4,10967
103
103
  schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
104
104
  schemathesis/specs/openapi/converter.py,sha256=TaYgc5BBHPdkN-n0lqpbeVgLu3eL3L8Wu3y_Vo3TJaQ,2800
105
105
  schemathesis/specs/openapi/definitions.py,sha256=Z186F0gNBSCmPg-Kk7Q-n6XxEZHIOzgUyeqixlC62XE,94058
@@ -110,9 +110,9 @@ schemathesis/specs/openapi/links.py,sha256=2ucOLs50OhCqu0PEdbT_BGUM3fKnHBl97YGIS
110
110
  schemathesis/specs/openapi/loaders.py,sha256=JJdIz1aT03J9WmUWTLOz6Yhuu69IqmhobQ9_vL6XJ6U,24916
111
111
  schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
112
112
  schemathesis/specs/openapi/parameters.py,sha256=_6vNCnPXcdxjfAQbykCRLHjvmTpu_02xDJghxDrGYr8,13611
113
- schemathesis/specs/openapi/references.py,sha256=i1Jd8EtEpSpauFW29DQsYM-JvB4jtYI7dpJAJcdjYe0,9276
114
- schemathesis/specs/openapi/schemas.py,sha256=8nzbHG27VoubYXrIdmY9rBUKovS1RpVY6FTgSWE1aWQ,52031
115
- schemathesis/specs/openapi/security.py,sha256=sTOCYEsJmxGY2dglrlZRYmJzNZjEZAHm5bZtbYFHLmI,6612
113
+ schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
114
+ schemathesis/specs/openapi/schemas.py,sha256=sZXzw4ToOpTbrjFQdvCwGymaCWW1b_ofzm7jJQK03kI,52287
115
+ schemathesis/specs/openapi/security.py,sha256=nEhDB_SvEFldmfpa9uOQywfWN6DtXHKmgtwucJvfN5Q,7096
116
116
  schemathesis/specs/openapi/serialization.py,sha256=5qGdFHZ3n80UlbSXrO_bkr4Al_7ci_Z3aSUjZczNDQY,11384
117
117
  schemathesis/specs/openapi/utils.py,sha256=-TCu0hTrlwp2x5qHNp-TxiHRMeIZC9OBmlhLssjRIiQ,742
118
118
  schemathesis/specs/openapi/validation.py,sha256=Q9ThZlwU-mSz7ExDnIivnZGi1ivC5hlX2mIMRAM79kc,999
@@ -124,7 +124,7 @@ schemathesis/specs/openapi/expressions/lexer.py,sha256=LeVE6fgYT9-fIsXrv0-YrRHnI
124
124
  schemathesis/specs/openapi/expressions/nodes.py,sha256=DUbAtuXdUDsxZ_pGeCVXAlL3gTj8nt9KulMGaIS-N2I,3948
125
125
  schemathesis/specs/openapi/expressions/parser.py,sha256=gM_Ob-TlTGxpgjZGRHNyPhBj1YAvRgRoSlNCrE7-djk,4452
126
126
  schemathesis/specs/openapi/negative/__init__.py,sha256=gw0w_9tVQf_MY5Df3_xTZFC4rAy1TTBS4wBccm36uFs,3697
127
- schemathesis/specs/openapi/negative/mutations.py,sha256=JRTkJNO9njma3xTYYoxSVpQgz-CfeMBLv4NDNPGiogI,18644
127
+ schemathesis/specs/openapi/negative/mutations.py,sha256=lLEN0GLxvPmZBQ3tHCznDSjmZ4yQiQxspjv1UpO4Kx0,19019
128
128
  schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
129
129
  schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
130
130
  schemathesis/specs/openapi/stateful/__init__.py,sha256=fAA52Nk0olm46u1e6OtZTXwmFM5W2r0jhRhZ24iz7GY,7673
@@ -132,20 +132,20 @@ schemathesis/specs/openapi/stateful/statistic.py,sha256=EJK4NqeAYRYl1FtU9YEuTLyh
132
132
  schemathesis/specs/openapi/stateful/types.py,sha256=UuGcCTFvaHsqeLN9ZeUNcbjsEwmthoT3UcHfDHchOYo,419
133
133
  schemathesis/stateful/__init__.py,sha256=qyQJ-9Ect-AWZiAsK63F3BTGu-jZnPCOp1q46YAonkQ,4911
134
134
  schemathesis/stateful/config.py,sha256=kuKGLNNk0YrR6G0IzcZh6v0oysHHTPfHi9nHUsfQsF0,2298
135
- schemathesis/stateful/context.py,sha256=P2kVeMOMsHOpQ0wyeZ2OfTU5fSCgehKJxzai1_tXiwc,3747
136
- schemathesis/stateful/events.py,sha256=wXh4O9FELmNWAUnupAFpUFOokjesvQOE8joBUAqjbrU,4947
137
- schemathesis/stateful/runner.py,sha256=ZcnU0lERjenpG0XnQUSeHH0MOesCg7K97fE53eIxvvc,9633
138
- schemathesis/stateful/sink.py,sha256=V1ReRTpN3VfAXtcnSEke4UHhu7IxrYQs0s_gSDMaqWw,2425
135
+ schemathesis/stateful/context.py,sha256=nYurdKz3yLA4xvOAUBc5hiJikDaMTiIn69RQhskreVU,3932
136
+ schemathesis/stateful/events.py,sha256=VF0jRi4eq5ybQnyIjSvScRMjQK-NwKd-pNZ7ZVJwygk,5215
137
+ schemathesis/stateful/runner.py,sha256=4VDjo_PgSoNxfMVsAOfXlJVXZPhqh8fc76Lt7cXPXZ4,9404
138
+ schemathesis/stateful/sink.py,sha256=yWY9xJeUyOWOsu1tNxCgsDFQVJxK2xgQavJ9vQoxK1I,2441
139
139
  schemathesis/stateful/state_machine.py,sha256=H-AzMPTKuCKnoCv0b7XPFDsHkzRftNfbvh5xb2H5Hfk,12156
140
140
  schemathesis/stateful/statistic.py,sha256=xPLiCw61ofNXQicqcK_sZyLHiqiGcgQARpwd8AiRubM,487
141
- schemathesis/stateful/validation.py,sha256=tqUtd9lEq4ZbJf1vA8hRxj_tO6l9B1DM-SRVVB6qKd8,2324
141
+ schemathesis/stateful/validation.py,sha256=RiXnZBTu6zMnjCpZBLyMwPDJrPTnSHWSijrg34VtRoM,2825
142
142
  schemathesis/transports/__init__.py,sha256=fwICPJBLHA7_L4IDAiW1SmxNHXlLD_Eqp-74w9TRRIs,12160
143
143
  schemathesis/transports/auth.py,sha256=4z7c-K7lfyyVqgR6X1v4yiE8ewR_ViAznWFTAsCL0RI,405
144
144
  schemathesis/transports/content_types.py,sha256=VrcRQvF5T_TUjrCyrZcYF2LOwKfs3IrLcMtkVSp1ImI,2189
145
145
  schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
146
146
  schemathesis/transports/responses.py,sha256=6-gvVcRK0Ho_lSydUysBNFWoJwZEiEgf6Iv-GWkQGd8,1675
147
- schemathesis-3.30.4.dist-info/METADATA,sha256=AD5b4TDRZ1kayE-ZWdgV3RN3jLO7c-KHXdJPo_Ux2RM,17671
148
- schemathesis-3.30.4.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
149
- schemathesis-3.30.4.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
150
- schemathesis-3.30.4.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
151
- schemathesis-3.30.4.dist-info/RECORD,,
147
+ schemathesis-3.31.1.dist-info/METADATA,sha256=W2PcjGom3XH8VuWRWR7kqLv12ZUliovQllOYeyMsnqk,17706
148
+ schemathesis-3.31.1.dist-info/WHEEL,sha256=hKi7AIIx6qfnsRbr087vpeJnrVUuDokDHZacPPMW7-Y,87
149
+ schemathesis-3.31.1.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
150
+ schemathesis-3.31.1.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
151
+ schemathesis-3.31.1.dist-info/RECORD,,