schemathesis 3.36.1__py3-none-any.whl → 3.36.3__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.
- schemathesis/_hypothesis.py +15 -5
- schemathesis/cli/cassettes.py +10 -0
- schemathesis/generation/coverage.py +139 -80
- schemathesis/internal/checks.py +3 -2
- schemathesis/models.py +5 -1
- schemathesis/runner/serialization.py +2 -0
- schemathesis/specs/openapi/_hypothesis.py +7 -1
- schemathesis/specs/openapi/checks.py +1 -1
- schemathesis/specs/openapi/converter.py +10 -0
- schemathesis/specs/openapi/patterns.py +120 -0
- {schemathesis-3.36.1.dist-info → schemathesis-3.36.3.dist-info}/METADATA +1 -1
- {schemathesis-3.36.1.dist-info → schemathesis-3.36.3.dist-info}/RECORD +15 -14
- {schemathesis-3.36.1.dist-info → schemathesis-3.36.3.dist-info}/WHEEL +0 -0
- {schemathesis-3.36.1.dist-info → schemathesis-3.36.3.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.36.1.dist-info → schemathesis-3.36.3.dist-info}/licenses/LICENSE +0 -0
schemathesis/_hypothesis.py
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
|
+
from copy import copy
|
|
7
8
|
import warnings
|
|
8
9
|
from typing import TYPE_CHECKING, Any, Callable, Generator, Mapping
|
|
9
10
|
|
|
@@ -221,7 +222,13 @@ def _iter_coverage_cases(
|
|
|
221
222
|
|
|
222
223
|
ctx = coverage.CoverageContext(data_generation_methods=data_generation_methods)
|
|
223
224
|
meta = GenerationMetadata(
|
|
224
|
-
query=None,
|
|
225
|
+
query=None,
|
|
226
|
+
path_parameters=None,
|
|
227
|
+
headers=None,
|
|
228
|
+
cookies=None,
|
|
229
|
+
body=None,
|
|
230
|
+
phase=TestPhase.COVERAGE,
|
|
231
|
+
description=None,
|
|
225
232
|
)
|
|
226
233
|
generators: dict[tuple[str, str], Generator[coverage.GeneratedValue, None, None]] = {}
|
|
227
234
|
template: dict[str, Any] = {}
|
|
@@ -259,17 +266,19 @@ def _iter_coverage_cases(
|
|
|
259
266
|
template["media_type"] = body.media_type
|
|
260
267
|
case = operation.make_case(**{**template, "body": value.value, "media_type": body.media_type})
|
|
261
268
|
case.data_generation_method = value.data_generation_method
|
|
262
|
-
case.meta = meta
|
|
269
|
+
case.meta = copy(meta)
|
|
270
|
+
case.meta.description = value.description
|
|
263
271
|
yield case
|
|
264
272
|
for next_value in gen:
|
|
265
273
|
case = operation.make_case(**{**template, "body": next_value.value, "media_type": body.media_type})
|
|
266
274
|
case.data_generation_method = next_value.data_generation_method
|
|
267
|
-
case.meta = meta
|
|
275
|
+
case.meta = copy(meta)
|
|
276
|
+
case.meta.description = next_value.description
|
|
268
277
|
yield case
|
|
269
278
|
elif DataGenerationMethod.positive in data_generation_methods:
|
|
270
279
|
case = operation.make_case(**template)
|
|
271
280
|
case.data_generation_method = DataGenerationMethod.positive
|
|
272
|
-
case.meta = meta
|
|
281
|
+
case.meta = copy(meta)
|
|
273
282
|
yield case
|
|
274
283
|
for (location, name), gen in generators.items():
|
|
275
284
|
container_name = LOCATION_TO_CONTAINER[location]
|
|
@@ -281,7 +290,8 @@ def _iter_coverage_cases(
|
|
|
281
290
|
generated = value.value
|
|
282
291
|
case = operation.make_case(**{**template, container_name: {**container, name: generated}})
|
|
283
292
|
case.data_generation_method = value.data_generation_method
|
|
284
|
-
case.meta = meta
|
|
293
|
+
case.meta = copy(meta)
|
|
294
|
+
case.meta.description = value.description
|
|
285
295
|
yield case
|
|
286
296
|
|
|
287
297
|
|
schemathesis/cli/cassettes.py
CHANGED
|
@@ -239,6 +239,16 @@ http_interactions:"""
|
|
|
239
239
|
thread_id: {item.thread_id}
|
|
240
240
|
correlation_id: '{item.correlation_id}'
|
|
241
241
|
data_generation_method: '{interaction.data_generation_method.value}'
|
|
242
|
+
meta:
|
|
243
|
+
description: """
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if interaction.description is not None:
|
|
247
|
+
write_double_quoted(stream, interaction.description)
|
|
248
|
+
else:
|
|
249
|
+
stream.write("null")
|
|
250
|
+
stream.write(
|
|
251
|
+
f"""
|
|
242
252
|
phase: {phase}
|
|
243
253
|
elapsed: '{interaction.response.elapsed if interaction.response else 0}'
|
|
244
254
|
recorded_at: '{interaction.recorded_at}'
|
|
@@ -15,7 +15,7 @@ from hypothesis_jsonschema._canonicalise import canonicalish
|
|
|
15
15
|
|
|
16
16
|
from schemathesis.constants import NOT_SET
|
|
17
17
|
|
|
18
|
-
from ._hypothesis import
|
|
18
|
+
from ._hypothesis import get_single_example
|
|
19
19
|
from ._methods import DataGenerationMethod
|
|
20
20
|
|
|
21
21
|
BUFFER_SIZE = 8 * 1024
|
|
@@ -36,16 +36,17 @@ UNKNOWN_PROPERTY_VALUE = 42
|
|
|
36
36
|
class GeneratedValue:
|
|
37
37
|
value: Any
|
|
38
38
|
data_generation_method: DataGenerationMethod
|
|
39
|
+
description: str
|
|
39
40
|
|
|
40
|
-
__slots__ = ("value", "data_generation_method")
|
|
41
|
+
__slots__ = ("value", "data_generation_method", "description")
|
|
41
42
|
|
|
42
43
|
@classmethod
|
|
43
|
-
def with_positive(cls, value: Any) -> GeneratedValue:
|
|
44
|
-
return cls(value, DataGenerationMethod.positive)
|
|
44
|
+
def with_positive(cls, value: Any, *, description: str) -> GeneratedValue:
|
|
45
|
+
return cls(value=value, data_generation_method=DataGenerationMethod.positive, description=description)
|
|
45
46
|
|
|
46
47
|
@classmethod
|
|
47
|
-
def with_negative(cls, value: Any) -> GeneratedValue:
|
|
48
|
-
return cls(value, DataGenerationMethod.negative)
|
|
48
|
+
def with_negative(cls, value: Any, *, description: str) -> GeneratedValue:
|
|
49
|
+
return cls(value=value, data_generation_method=DataGenerationMethod.negative, description=description)
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
PositiveValue = GeneratedValue.with_positive
|
|
@@ -117,15 +118,15 @@ def _cover_positive_for_type(
|
|
|
117
118
|
yield from cover_schema_iter(ctx, canonical)
|
|
118
119
|
if enum is not NOT_SET:
|
|
119
120
|
for value in enum:
|
|
120
|
-
yield PositiveValue(value)
|
|
121
|
+
yield PositiveValue(value, description="Enum value")
|
|
121
122
|
elif const is not NOT_SET:
|
|
122
|
-
yield PositiveValue(const)
|
|
123
|
+
yield PositiveValue(const, description="Const value")
|
|
123
124
|
elif ty is not None:
|
|
124
125
|
if ty == "null":
|
|
125
|
-
yield PositiveValue(None)
|
|
126
|
+
yield PositiveValue(None, description="Value null value")
|
|
126
127
|
elif ty == "boolean":
|
|
127
|
-
yield PositiveValue(True)
|
|
128
|
-
yield PositiveValue(False)
|
|
128
|
+
yield PositiveValue(True, description="Valid boolean value")
|
|
129
|
+
yield PositiveValue(False, description="Valid boolean value")
|
|
129
130
|
elif ty == "string":
|
|
130
131
|
yield from _positive_string(ctx, schema)
|
|
131
132
|
elif ty == "integer" or ty == "number":
|
|
@@ -157,7 +158,11 @@ def _ignore_unfixable(
|
|
|
157
158
|
raise
|
|
158
159
|
|
|
159
160
|
|
|
160
|
-
def cover_schema_iter(
|
|
161
|
+
def cover_schema_iter(
|
|
162
|
+
ctx: CoverageContext, schema: dict | bool, seen: set[Any | tuple[type, str]] | None = None
|
|
163
|
+
) -> Generator[GeneratedValue, None, None]:
|
|
164
|
+
if seen is None:
|
|
165
|
+
seen = set()
|
|
161
166
|
if isinstance(schema, bool):
|
|
162
167
|
types = ["null", "boolean", "string", "number", "array", "object"]
|
|
163
168
|
schema = {}
|
|
@@ -174,13 +179,16 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
174
179
|
yield from _cover_positive_for_type(ctx, schema, ty)
|
|
175
180
|
if DataGenerationMethod.negative in ctx.data_generation_methods:
|
|
176
181
|
template = None
|
|
177
|
-
seen: set[Any | tuple[type, str]] = set()
|
|
178
182
|
for key, value in schema.items():
|
|
179
183
|
with _ignore_unfixable():
|
|
180
184
|
if key == "enum":
|
|
181
185
|
yield from _negative_enum(ctx, value)
|
|
182
186
|
elif key == "const":
|
|
183
|
-
|
|
187
|
+
for value_ in _negative_enum(ctx, [value]):
|
|
188
|
+
k = _to_hashable_key(value_.value)
|
|
189
|
+
if k not in seen:
|
|
190
|
+
yield value_
|
|
191
|
+
seen.add(k)
|
|
184
192
|
elif key == "type":
|
|
185
193
|
yield from _negative_type(ctx, seen, value)
|
|
186
194
|
elif key == "properties":
|
|
@@ -192,27 +200,39 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
192
200
|
yield from _negative_format(ctx, schema, value)
|
|
193
201
|
elif key == "maximum":
|
|
194
202
|
next = value + 1
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
if next not in seen:
|
|
204
|
+
yield NegativeValue(next, description="Value greater than maximum")
|
|
205
|
+
seen.add(next)
|
|
197
206
|
elif key == "minimum":
|
|
198
207
|
next = value - 1
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
if next not in seen:
|
|
209
|
+
yield NegativeValue(next, description="Value smaller than minimum")
|
|
210
|
+
seen.add(next)
|
|
201
211
|
elif key == "exclusiveMaximum" or key == "exclusiveMinimum" and value not in seen:
|
|
202
|
-
|
|
212
|
+
verb = "greater" if key == "exclusiveMaximum" else "smaller"
|
|
213
|
+
limit = "maximum" if key == "exclusiveMaximum" else "minimum"
|
|
214
|
+
yield NegativeValue(value, description=f"Value {verb} than {limit}")
|
|
203
215
|
seen.add(value)
|
|
204
216
|
elif key == "multipleOf":
|
|
205
|
-
|
|
217
|
+
for value_ in _negative_multiple_of(ctx, schema, value):
|
|
218
|
+
k = _to_hashable_key(value_.value)
|
|
219
|
+
if k not in seen:
|
|
220
|
+
yield value_
|
|
221
|
+
seen.add(k)
|
|
206
222
|
elif key == "minLength" and 0 < value < BUFFER_SIZE:
|
|
207
223
|
with suppress(InvalidArgument):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
value = ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
|
|
225
|
+
k = _to_hashable_key(value)
|
|
226
|
+
if k not in seen:
|
|
227
|
+
yield NegativeValue(value, description="String smaller than minLength")
|
|
228
|
+
seen.add(k)
|
|
211
229
|
elif key == "maxLength" and value < BUFFER_SIZE:
|
|
212
230
|
with suppress(InvalidArgument):
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
231
|
+
value = ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
|
|
232
|
+
k = _to_hashable_key(value)
|
|
233
|
+
if k not in seen:
|
|
234
|
+
yield NegativeValue(value, description="String larger than maxLength")
|
|
235
|
+
seen.add(k)
|
|
216
236
|
elif key == "uniqueItems" and value:
|
|
217
237
|
yield from _negative_unique_items(ctx, schema)
|
|
218
238
|
elif key == "required":
|
|
@@ -220,20 +240,23 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
|
|
|
220
240
|
yield from _negative_required(ctx, template, value)
|
|
221
241
|
elif key == "additionalProperties" and not value:
|
|
222
242
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
|
223
|
-
yield NegativeValue(
|
|
243
|
+
yield NegativeValue(
|
|
244
|
+
{**template, UNKNOWN_PROPERTY_KEY: UNKNOWN_PROPERTY_VALUE},
|
|
245
|
+
description="Object with unexpected properties",
|
|
246
|
+
)
|
|
224
247
|
elif key == "allOf":
|
|
225
248
|
nctx = ctx.with_negative()
|
|
226
249
|
if len(value) == 1:
|
|
227
|
-
yield from cover_schema_iter(nctx, value[0])
|
|
250
|
+
yield from cover_schema_iter(nctx, value[0], seen)
|
|
228
251
|
else:
|
|
229
252
|
with _ignore_unfixable():
|
|
230
253
|
canonical = canonicalish(schema)
|
|
231
|
-
yield from cover_schema_iter(nctx, canonical)
|
|
254
|
+
yield from cover_schema_iter(nctx, canonical, seen)
|
|
232
255
|
elif key == "anyOf" or key == "oneOf":
|
|
233
256
|
nctx = ctx.with_negative()
|
|
234
257
|
# NOTE: Other sub-schemas are not filtered out
|
|
235
258
|
for sub_schema in value:
|
|
236
|
-
yield from cover_schema_iter(nctx, sub_schema)
|
|
259
|
+
yield from cover_schema_iter(nctx, sub_schema, seen)
|
|
237
260
|
|
|
238
261
|
|
|
239
262
|
def _get_properties(schema: dict | bool) -> dict | bool:
|
|
@@ -274,43 +297,50 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
274
297
|
default = schema.get("default")
|
|
275
298
|
if example or examples or default:
|
|
276
299
|
if example:
|
|
277
|
-
yield PositiveValue(example)
|
|
300
|
+
yield PositiveValue(example, description="Example value")
|
|
278
301
|
if examples:
|
|
279
302
|
for example in examples:
|
|
280
|
-
yield PositiveValue(example)
|
|
303
|
+
yield PositiveValue(example, description="Example value")
|
|
281
304
|
if (
|
|
282
305
|
default
|
|
283
306
|
and not (example is not None and default == example)
|
|
284
307
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
285
308
|
):
|
|
286
|
-
yield PositiveValue(default)
|
|
309
|
+
yield PositiveValue(default, description="Default value")
|
|
287
310
|
elif not min_length and not max_length:
|
|
288
311
|
# Default positive value
|
|
289
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
312
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
|
290
313
|
elif "pattern" in schema:
|
|
291
314
|
# Without merging `maxLength` & `minLength` into a regex it is problematic
|
|
292
315
|
# to generate a valid value as the unredlying machinery will resort to filtering
|
|
293
316
|
# and it is unlikely that it will generate a string of that length
|
|
294
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
317
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid string")
|
|
295
318
|
return
|
|
296
319
|
|
|
297
320
|
seen = set()
|
|
298
321
|
|
|
299
322
|
if min_length is not None and min_length < BUFFER_SIZE:
|
|
300
323
|
# Exactly the minimum length
|
|
301
|
-
yield PositiveValue(
|
|
324
|
+
yield PositiveValue(
|
|
325
|
+
ctx.generate_from_schema({**schema, "maxLength": min_length}), description="Minimum length string"
|
|
326
|
+
)
|
|
302
327
|
seen.add(min_length)
|
|
303
328
|
|
|
304
329
|
# One character more than minimum if possible
|
|
305
330
|
larger = min_length + 1
|
|
306
331
|
if larger < BUFFER_SIZE and larger not in seen and (not max_length or larger <= max_length):
|
|
307
|
-
yield PositiveValue(
|
|
332
|
+
yield PositiveValue(
|
|
333
|
+
ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}),
|
|
334
|
+
description="Near-boundary length string",
|
|
335
|
+
)
|
|
308
336
|
seen.add(larger)
|
|
309
337
|
|
|
310
338
|
if max_length is not None:
|
|
311
339
|
# Exactly the maximum length
|
|
312
340
|
if max_length < BUFFER_SIZE and max_length not in seen:
|
|
313
|
-
yield PositiveValue(
|
|
341
|
+
yield PositiveValue(
|
|
342
|
+
ctx.generate_from_schema({**schema, "minLength": max_length}), description="Maximum length string"
|
|
343
|
+
)
|
|
314
344
|
seen.add(max_length)
|
|
315
345
|
|
|
316
346
|
# One character less than maximum if possible
|
|
@@ -320,7 +350,10 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
320
350
|
and smaller not in seen
|
|
321
351
|
and (smaller > 0 and (min_length is None or smaller >= min_length))
|
|
322
352
|
):
|
|
323
|
-
yield PositiveValue(
|
|
353
|
+
yield PositiveValue(
|
|
354
|
+
ctx.generate_from_schema({**schema, "minLength": smaller, "maxLength": smaller}),
|
|
355
|
+
description="Near-boundary length string",
|
|
356
|
+
)
|
|
324
357
|
seen.add(smaller)
|
|
325
358
|
|
|
326
359
|
|
|
@@ -350,19 +383,19 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
350
383
|
|
|
351
384
|
if example or examples or default:
|
|
352
385
|
if example:
|
|
353
|
-
yield PositiveValue(example)
|
|
386
|
+
yield PositiveValue(example, description="Example value")
|
|
354
387
|
if examples:
|
|
355
388
|
for example in examples:
|
|
356
|
-
yield PositiveValue(example)
|
|
389
|
+
yield PositiveValue(example, description="Example value")
|
|
357
390
|
if (
|
|
358
391
|
default
|
|
359
392
|
and not (example is not None and default == example)
|
|
360
393
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
361
394
|
):
|
|
362
|
-
yield PositiveValue(default)
|
|
395
|
+
yield PositiveValue(default, description="Default value")
|
|
363
396
|
elif not minimum and not maximum:
|
|
364
397
|
# Default positive value
|
|
365
|
-
yield PositiveValue(ctx.generate_from_schema(schema))
|
|
398
|
+
yield PositiveValue(ctx.generate_from_schema(schema), description="Valid number")
|
|
366
399
|
|
|
367
400
|
seen = set()
|
|
368
401
|
|
|
@@ -373,7 +406,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
373
406
|
else:
|
|
374
407
|
smallest = minimum
|
|
375
408
|
seen.add(smallest)
|
|
376
|
-
yield PositiveValue(smallest)
|
|
409
|
+
yield PositiveValue(smallest, description="Minimum value")
|
|
377
410
|
|
|
378
411
|
# One more than minimum if possible
|
|
379
412
|
if multiple_of is not None:
|
|
@@ -382,7 +415,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
382
415
|
larger = minimum + 1
|
|
383
416
|
if larger not in seen and (not maximum or larger <= maximum):
|
|
384
417
|
seen.add(larger)
|
|
385
|
-
yield PositiveValue(larger)
|
|
418
|
+
yield PositiveValue(larger, description="Near-boundary number")
|
|
386
419
|
|
|
387
420
|
if maximum is not None:
|
|
388
421
|
# Exactly the maximum
|
|
@@ -392,7 +425,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
392
425
|
largest = maximum
|
|
393
426
|
if largest not in seen:
|
|
394
427
|
seen.add(largest)
|
|
395
|
-
yield PositiveValue(largest)
|
|
428
|
+
yield PositiveValue(largest, description="Maximum value")
|
|
396
429
|
|
|
397
430
|
# One less than maximum if possible
|
|
398
431
|
if multiple_of is not None:
|
|
@@ -401,7 +434,7 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
|
|
|
401
434
|
smaller = maximum - 1
|
|
402
435
|
if smaller not in seen and (smaller > 0 and (minimum is None or smaller >= minimum)):
|
|
403
436
|
seen.add(smaller)
|
|
404
|
-
yield PositiveValue(smaller)
|
|
437
|
+
yield PositiveValue(smaller, description="Near-boundary number")
|
|
405
438
|
|
|
406
439
|
|
|
407
440
|
def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
|
|
@@ -412,18 +445,18 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
412
445
|
|
|
413
446
|
if example or examples or default:
|
|
414
447
|
if example:
|
|
415
|
-
yield PositiveValue(example)
|
|
448
|
+
yield PositiveValue(example, description="Example value")
|
|
416
449
|
if examples:
|
|
417
450
|
for example in examples:
|
|
418
|
-
yield PositiveValue(example)
|
|
451
|
+
yield PositiveValue(example, description="Example value")
|
|
419
452
|
if (
|
|
420
453
|
default
|
|
421
454
|
and not (example is not None and default == example)
|
|
422
455
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
423
456
|
):
|
|
424
|
-
yield PositiveValue(default)
|
|
457
|
+
yield PositiveValue(default, description="Default value")
|
|
425
458
|
else:
|
|
426
|
-
yield PositiveValue(template)
|
|
459
|
+
yield PositiveValue(template, description="Valid array")
|
|
427
460
|
seen.add(len(template))
|
|
428
461
|
|
|
429
462
|
# Boundary and near-boundary sizes
|
|
@@ -435,12 +468,18 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
435
468
|
# One item more than minimum if possible
|
|
436
469
|
larger = min_items + 1
|
|
437
470
|
if larger not in seen and (max_items is None or larger <= max_items):
|
|
438
|
-
yield PositiveValue(
|
|
471
|
+
yield PositiveValue(
|
|
472
|
+
ctx.generate_from_schema({**schema, "minItems": larger, "maxItems": larger}),
|
|
473
|
+
description="Near-boundary items array",
|
|
474
|
+
)
|
|
439
475
|
seen.add(larger)
|
|
440
476
|
|
|
441
477
|
if max_items is not None:
|
|
442
478
|
if max_items < BUFFER_SIZE and max_items not in seen:
|
|
443
|
-
yield PositiveValue(
|
|
479
|
+
yield PositiveValue(
|
|
480
|
+
ctx.generate_from_schema({**schema, "minItems": max_items}),
|
|
481
|
+
description="Maximum items array",
|
|
482
|
+
)
|
|
444
483
|
seen.add(max_items)
|
|
445
484
|
|
|
446
485
|
# One item smaller than maximum if possible
|
|
@@ -451,7 +490,10 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
|
|
|
451
490
|
and smaller not in seen
|
|
452
491
|
and (min_items is None or smaller >= min_items)
|
|
453
492
|
):
|
|
454
|
-
yield PositiveValue(
|
|
493
|
+
yield PositiveValue(
|
|
494
|
+
ctx.generate_from_schema({**schema, "minItems": smaller, "maxItems": smaller}),
|
|
495
|
+
description="Near-boundary items array",
|
|
496
|
+
)
|
|
455
497
|
seen.add(smaller)
|
|
456
498
|
|
|
457
499
|
|
|
@@ -462,19 +504,18 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
|
462
504
|
|
|
463
505
|
if example or examples or default:
|
|
464
506
|
if example:
|
|
465
|
-
yield PositiveValue(example)
|
|
507
|
+
yield PositiveValue(example, description="Example value")
|
|
466
508
|
if examples:
|
|
467
509
|
for example in examples:
|
|
468
|
-
yield PositiveValue(example)
|
|
510
|
+
yield PositiveValue(example, description="Example value")
|
|
469
511
|
if (
|
|
470
512
|
default
|
|
471
513
|
and not (example is not None and default == example)
|
|
472
514
|
and not (examples is not None and any(default == ex for ex in examples))
|
|
473
515
|
):
|
|
474
|
-
yield PositiveValue(default)
|
|
475
|
-
|
|
516
|
+
yield PositiveValue(default, description="Default value")
|
|
476
517
|
else:
|
|
477
|
-
yield PositiveValue(template)
|
|
518
|
+
yield PositiveValue(template, description="Valid object")
|
|
478
519
|
|
|
479
520
|
properties = schema.get("properties", {})
|
|
480
521
|
required = set(schema.get("required", []))
|
|
@@ -485,22 +526,24 @@ def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Gene
|
|
|
485
526
|
for name in optional:
|
|
486
527
|
combo = {k: v for k, v in template.items() if k in required or k == name}
|
|
487
528
|
if combo != template:
|
|
488
|
-
yield PositiveValue(combo)
|
|
529
|
+
yield PositiveValue(combo, description=f"Object with all required properties and '{name}'")
|
|
489
530
|
# Generate one combination for each size from 2 to N-1
|
|
490
531
|
for selection in select_combinations(optional):
|
|
491
532
|
combo = {k: v for k, v in template.items() if k in required or k in selection}
|
|
492
|
-
yield PositiveValue(combo)
|
|
533
|
+
yield PositiveValue(combo, description="Object with all required and a subset of optional properties")
|
|
493
534
|
# Generate only required properties
|
|
494
535
|
if set(properties) != required:
|
|
495
536
|
only_required = {k: v for k, v in template.items() if k in required}
|
|
496
|
-
yield PositiveValue(only_required)
|
|
537
|
+
yield PositiveValue(only_required, description="Object with only required properties")
|
|
497
538
|
seen = set()
|
|
498
539
|
for name, sub_schema in properties.items():
|
|
499
540
|
seen.add(_to_hashable_key(template.get(name)))
|
|
500
541
|
for new in cover_schema_iter(ctx, sub_schema):
|
|
501
542
|
key = _to_hashable_key(new.value)
|
|
502
543
|
if key not in seen:
|
|
503
|
-
yield PositiveValue(
|
|
544
|
+
yield PositiveValue(
|
|
545
|
+
{**template, name: new.value}, description=f"Object with valid '{name}' value: {new.description}"
|
|
546
|
+
)
|
|
504
547
|
seen.add(key)
|
|
505
548
|
seen.clear()
|
|
506
549
|
|
|
@@ -513,7 +556,7 @@ def select_combinations(optional: list[str]) -> Iterator[tuple[str, ...]]:
|
|
|
513
556
|
def _negative_enum(ctx: CoverageContext, value: list) -> Generator[GeneratedValue, None, None]:
|
|
514
557
|
strategy = JSON_STRATEGY.filter(lambda x: x not in value)
|
|
515
558
|
# The exact negative value is not important here
|
|
516
|
-
yield NegativeValue(ctx.generate_from(strategy, cached=True))
|
|
559
|
+
yield NegativeValue(ctx.generate_from(strategy, cached=True), description="Invalid enum value")
|
|
517
560
|
|
|
518
561
|
|
|
519
562
|
def _negative_properties(
|
|
@@ -522,11 +565,17 @@ def _negative_properties(
|
|
|
522
565
|
nctx = ctx.with_negative()
|
|
523
566
|
for key, sub_schema in properties.items():
|
|
524
567
|
for value in cover_schema_iter(nctx, sub_schema):
|
|
525
|
-
yield NegativeValue(
|
|
568
|
+
yield NegativeValue(
|
|
569
|
+
{**template, key: value.value},
|
|
570
|
+
description=f"Object with invalid '{key}' value: {value.description}",
|
|
571
|
+
)
|
|
526
572
|
|
|
527
573
|
|
|
528
574
|
def _negative_pattern(ctx: CoverageContext, pattern: str) -> Generator[GeneratedValue, None, None]:
|
|
529
|
-
yield NegativeValue(
|
|
575
|
+
yield NegativeValue(
|
|
576
|
+
ctx.generate_from(st.text().filter(lambda x: x != pattern), cached=True),
|
|
577
|
+
description=f"Value not matching the '{pattern}' pattern",
|
|
578
|
+
)
|
|
530
579
|
|
|
531
580
|
|
|
532
581
|
def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
@@ -536,19 +585,25 @@ def _with_negated_key(schema: dict, key: str, value: Any) -> dict:
|
|
|
536
585
|
def _negative_multiple_of(
|
|
537
586
|
ctx: CoverageContext, schema: dict, multiple_of: int | float
|
|
538
587
|
) -> Generator[GeneratedValue, None, None]:
|
|
539
|
-
yield NegativeValue(
|
|
588
|
+
yield NegativeValue(
|
|
589
|
+
ctx.generate_from_schema(_with_negated_key(schema, "multipleOf", multiple_of)),
|
|
590
|
+
description=f"Non-multiple of {multiple_of}",
|
|
591
|
+
)
|
|
540
592
|
|
|
541
593
|
|
|
542
594
|
def _negative_unique_items(ctx: CoverageContext, schema: dict) -> Generator[GeneratedValue, None, None]:
|
|
543
595
|
unique = ctx.generate_from_schema({**schema, "type": "array", "minItems": 1, "maxItems": 1})
|
|
544
|
-
yield NegativeValue(unique + unique)
|
|
596
|
+
yield NegativeValue(unique + unique, description="Non-unique items")
|
|
545
597
|
|
|
546
598
|
|
|
547
599
|
def _negative_required(
|
|
548
600
|
ctx: CoverageContext, template: dict, required: list[str]
|
|
549
601
|
) -> Generator[GeneratedValue, None, None]:
|
|
550
602
|
for key in required:
|
|
551
|
-
yield NegativeValue(
|
|
603
|
+
yield NegativeValue(
|
|
604
|
+
{k: v for k, v in template.items() if k != key},
|
|
605
|
+
description=f"Missing required property: {key}",
|
|
606
|
+
)
|
|
552
607
|
|
|
553
608
|
|
|
554
609
|
def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generator[GeneratedValue, None, None]:
|
|
@@ -561,7 +616,7 @@ def _negative_format(ctx: CoverageContext, schema: dict, format: str) -> Generat
|
|
|
561
616
|
lambda v: (format == "hostname" and v == "")
|
|
562
617
|
or not jsonschema.Draft202012Validator.FORMAT_CHECKER.conforms(v, format)
|
|
563
618
|
)
|
|
564
|
-
yield NegativeValue(ctx.generate_from(strategy))
|
|
619
|
+
yield NegativeValue(ctx.generate_from(strategy), description=f"Value not matching the '{format}' format")
|
|
565
620
|
|
|
566
621
|
|
|
567
622
|
def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Generator[GeneratedValue, None, None]:
|
|
@@ -584,10 +639,13 @@ def _negative_type(ctx: CoverageContext, seen: set, ty: str | list[str]) -> Gene
|
|
|
584
639
|
del strategies["integer"]
|
|
585
640
|
if "integer" in types:
|
|
586
641
|
strategies["number"] = FLOAT_STRATEGY.filter(lambda x: x != int(x))
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
642
|
+
for strat in strategies.values():
|
|
643
|
+
value = ctx.generate_from(strat, cached=True)
|
|
644
|
+
hashed = _to_hashable_key(value)
|
|
645
|
+
if hashed in seen:
|
|
646
|
+
continue
|
|
647
|
+
yield NegativeValue(value, description="Incorrect type")
|
|
648
|
+
seen.add(hashed)
|
|
591
649
|
|
|
592
650
|
|
|
593
651
|
def push_examples_to_properties(schema: dict[str, Any]) -> None:
|
|
@@ -595,9 +653,10 @@ def push_examples_to_properties(schema: dict[str, Any]) -> None:
|
|
|
595
653
|
if "examples" in schema and "properties" in schema:
|
|
596
654
|
properties = schema["properties"]
|
|
597
655
|
for example in schema["examples"]:
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
properties[prop]["examples"]
|
|
656
|
+
if isinstance(example, dict):
|
|
657
|
+
for prop, value in example.items():
|
|
658
|
+
if prop in properties:
|
|
659
|
+
if "examples" not in properties[prop]:
|
|
660
|
+
properties[prop]["examples"] = []
|
|
661
|
+
if value not in schema["properties"][prop]["examples"]:
|
|
662
|
+
properties[prop]["examples"].append(value)
|
schemathesis/internal/checks.py
CHANGED
|
@@ -6,10 +6,11 @@ from dataclasses import dataclass
|
|
|
6
6
|
from typing import TYPE_CHECKING, Callable, Optional
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from
|
|
9
|
+
from requests.structures import CaseInsensitiveDict
|
|
10
|
+
|
|
10
11
|
from ..models import Case
|
|
11
12
|
from ..transports.responses import GenericResponse
|
|
12
|
-
from
|
|
13
|
+
from ..types import RawAuth
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
CheckFunction = Callable[["CheckContext", "GenericResponse", "Case"], Optional[bool]]
|
schemathesis/models.py
CHANGED
|
@@ -154,8 +154,9 @@ class GenerationMetadata:
|
|
|
154
154
|
cookies: DataGenerationMethod | None
|
|
155
155
|
body: DataGenerationMethod | None
|
|
156
156
|
phase: TestPhase
|
|
157
|
+
description: str | None
|
|
157
158
|
|
|
158
|
-
__slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase")
|
|
159
|
+
__slots__ = ("query", "path_parameters", "headers", "cookies", "body", "phase", "description")
|
|
159
160
|
|
|
160
161
|
|
|
161
162
|
@dataclass(repr=False)
|
|
@@ -1017,6 +1018,7 @@ class Interaction:
|
|
|
1017
1018
|
status: Status
|
|
1018
1019
|
data_generation_method: DataGenerationMethod
|
|
1019
1020
|
phase: TestPhase | None
|
|
1021
|
+
description: str | None
|
|
1020
1022
|
recorded_at: str = field(default_factory=lambda: datetime.datetime.now(TIMEZONE).isoformat())
|
|
1021
1023
|
|
|
1022
1024
|
@classmethod
|
|
@@ -1046,6 +1048,7 @@ class Interaction:
|
|
|
1046
1048
|
checks=checks,
|
|
1047
1049
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
1048
1050
|
phase=case.meta.phase if case.meta is not None else None,
|
|
1051
|
+
description=case.meta.description if case.meta is not None else None,
|
|
1049
1052
|
)
|
|
1050
1053
|
|
|
1051
1054
|
@classmethod
|
|
@@ -1069,6 +1072,7 @@ class Interaction:
|
|
|
1069
1072
|
checks=checks,
|
|
1070
1073
|
data_generation_method=cast(DataGenerationMethod, case.data_generation_method),
|
|
1071
1074
|
phase=case.meta.phase if case.meta is not None else None,
|
|
1075
|
+
description=case.meta.description if case.meta is not None else None,
|
|
1072
1076
|
)
|
|
1073
1077
|
|
|
1074
1078
|
|
|
@@ -395,6 +395,7 @@ class SerializedInteraction:
|
|
|
395
395
|
status: Status
|
|
396
396
|
data_generation_method: DataGenerationMethod
|
|
397
397
|
phase: TestPhase | None
|
|
398
|
+
description: str | None
|
|
398
399
|
recorded_at: str
|
|
399
400
|
|
|
400
401
|
@classmethod
|
|
@@ -406,6 +407,7 @@ class SerializedInteraction:
|
|
|
406
407
|
status=interaction.status,
|
|
407
408
|
data_generation_method=interaction.data_generation_method,
|
|
408
409
|
phase=interaction.phase,
|
|
410
|
+
description=interaction.description,
|
|
409
411
|
recorded_at=interaction.recorded_at,
|
|
410
412
|
)
|
|
411
413
|
|
|
@@ -215,6 +215,7 @@ def get_case_strategy(
|
|
|
215
215
|
cookies=cookies_.generator,
|
|
216
216
|
body=body_.generator,
|
|
217
217
|
phase=phase,
|
|
218
|
+
description=None,
|
|
218
219
|
),
|
|
219
220
|
)
|
|
220
221
|
auth_context = auths.AuthContext(
|
|
@@ -527,7 +528,12 @@ def is_valid_path(parameters: dict[str, Any]) -> bool:
|
|
|
527
528
|
disallowed_values = (SLASH, "")
|
|
528
529
|
|
|
529
530
|
return not any(
|
|
530
|
-
(
|
|
531
|
+
(
|
|
532
|
+
value in disallowed_values
|
|
533
|
+
or is_illegal_surrogate(value)
|
|
534
|
+
or isinstance(value, str)
|
|
535
|
+
and (SLASH in value or "}" in value or "{" in value)
|
|
536
|
+
)
|
|
531
537
|
for value in parameters.values()
|
|
532
538
|
)
|
|
533
539
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
3
|
import enum
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from http.cookies import SimpleCookie
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
|
|
7
7
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -5,6 +5,7 @@ from typing import Any, Callable
|
|
|
5
5
|
|
|
6
6
|
from ...internal.copy import fast_deepcopy
|
|
7
7
|
from ...internal.jsonschema import traverse_schema
|
|
8
|
+
from .patterns import update_quantifier
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def to_json_schema(
|
|
@@ -24,6 +25,15 @@ def to_json_schema(
|
|
|
24
25
|
if schema_type == "file":
|
|
25
26
|
schema["type"] = "string"
|
|
26
27
|
schema["format"] = "binary"
|
|
28
|
+
pattern = schema.get("pattern")
|
|
29
|
+
min_length = schema.get("minLength")
|
|
30
|
+
max_length = schema.get("maxLength")
|
|
31
|
+
if pattern and (min_length or max_length):
|
|
32
|
+
new_pattern = update_quantifier(pattern, min_length, max_length)
|
|
33
|
+
if new_pattern != pattern:
|
|
34
|
+
schema.pop("minLength", None)
|
|
35
|
+
schema.pop("maxLength", None)
|
|
36
|
+
schema["pattern"] = new_pattern
|
|
27
37
|
if schema_type == "object":
|
|
28
38
|
if is_response_schema:
|
|
29
39
|
# Write-only properties should not occur in responses
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
try: # pragma: no cover
|
|
7
|
+
import re._constants as sre
|
|
8
|
+
import re._parser as sre_parse
|
|
9
|
+
except ImportError:
|
|
10
|
+
import sre_constants as sre
|
|
11
|
+
import sre_parse
|
|
12
|
+
|
|
13
|
+
ANCHOR = sre.AT
|
|
14
|
+
REPEATS: tuple
|
|
15
|
+
if hasattr(sre, "POSSESSIVE_REPEAT"):
|
|
16
|
+
REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT, sre.POSSESSIVE_REPEAT)
|
|
17
|
+
else:
|
|
18
|
+
REPEATS = (sre.MIN_REPEAT, sre.MAX_REPEAT)
|
|
19
|
+
LITERAL = sre.LITERAL
|
|
20
|
+
IN = sre.IN
|
|
21
|
+
MAXREPEAT = sre_parse.MAXREPEAT
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache()
|
|
25
|
+
def update_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
26
|
+
"""Update the quantifier of a regular expression based on given min and max lengths."""
|
|
27
|
+
if not pattern or (min_length in (None, 0) and max_length is None):
|
|
28
|
+
return pattern
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
parsed = sre_parse.parse(pattern)
|
|
32
|
+
return _handle_parsed_pattern(parsed, pattern, min_length, max_length)
|
|
33
|
+
except re.error:
|
|
34
|
+
# Invalid pattern
|
|
35
|
+
return pattern
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle_parsed_pattern(parsed: list, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
39
|
+
"""Handle the parsed pattern and update quantifiers based on different cases."""
|
|
40
|
+
if len(parsed) == 1:
|
|
41
|
+
op, value = parsed[0]
|
|
42
|
+
return _update_quantifier(op, value, pattern, min_length, max_length)
|
|
43
|
+
elif len(parsed) == 2:
|
|
44
|
+
if parsed[0][0] == ANCHOR:
|
|
45
|
+
# Starts with an anchor
|
|
46
|
+
op, value = parsed[1]
|
|
47
|
+
leading_anchor = pattern[0]
|
|
48
|
+
return leading_anchor + _update_quantifier(op, value, pattern[1:], min_length, max_length)
|
|
49
|
+
if parsed[1][0] == ANCHOR:
|
|
50
|
+
# Ends with an anchor
|
|
51
|
+
op, value = parsed[0]
|
|
52
|
+
trailing_anchor = pattern[-1]
|
|
53
|
+
return _update_quantifier(op, value, pattern[:-1], min_length, max_length) + trailing_anchor
|
|
54
|
+
elif len(parsed) == 3 and parsed[0][0] == ANCHOR and parsed[2][0] == ANCHOR:
|
|
55
|
+
op, value = parsed[1]
|
|
56
|
+
leading_anchor = pattern[0]
|
|
57
|
+
trailing_anchor = pattern[-1]
|
|
58
|
+
return leading_anchor + _update_quantifier(op, value, pattern[1:-1], min_length, max_length) + trailing_anchor
|
|
59
|
+
return pattern
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _update_quantifier(op: int, value: tuple, pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
63
|
+
"""Update the quantifier based on the operation type and given constraints."""
|
|
64
|
+
if op in REPEATS:
|
|
65
|
+
return _handle_repeat_quantifier(value, pattern, min_length, max_length)
|
|
66
|
+
if op in (LITERAL, IN) and max_length != 0:
|
|
67
|
+
return _handle_literal_or_in_quantifier(pattern, min_length, max_length)
|
|
68
|
+
return pattern
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _handle_repeat_quantifier(
|
|
72
|
+
value: tuple[int, int, tuple], pattern: str, min_length: int | None, max_length: int | None
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Handle repeat quantifiers (e.g., '+', '*', '?')."""
|
|
75
|
+
min_repeat, max_repeat, _ = value
|
|
76
|
+
min_length, max_length = _build_size(min_repeat, max_repeat, min_length, max_length)
|
|
77
|
+
if min_length > max_length:
|
|
78
|
+
return pattern
|
|
79
|
+
return f"({_strip_quantifier(pattern)})" + _build_quantifier(min_length, max_length)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_literal_or_in_quantifier(pattern: str, min_length: int | None, max_length: int | None) -> str:
|
|
83
|
+
"""Handle literal or character class quantifiers."""
|
|
84
|
+
min_length = 1 if min_length is None else max(min_length, 1)
|
|
85
|
+
return f"({pattern})" + _build_quantifier(min_length, max_length)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_quantifier(minimum: int | None, maximum: int | None) -> str:
|
|
89
|
+
"""Construct a quantifier string based on min and max values."""
|
|
90
|
+
if maximum == MAXREPEAT or maximum is None:
|
|
91
|
+
return f"{{{minimum or 0},}}"
|
|
92
|
+
if minimum == maximum:
|
|
93
|
+
return f"{{{minimum}}}"
|
|
94
|
+
return f"{{{minimum or 0},{maximum}}}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_size(min_repeat: int, max_repeat: int, min_length: int | None, max_length: int | None) -> tuple[int, int]:
|
|
98
|
+
"""Merge the current repetition constraints with the provided min and max lengths."""
|
|
99
|
+
if min_length is not None:
|
|
100
|
+
min_repeat = max(min_repeat, min_length)
|
|
101
|
+
if max_length is not None:
|
|
102
|
+
if max_repeat == MAXREPEAT:
|
|
103
|
+
max_repeat = max_length
|
|
104
|
+
else:
|
|
105
|
+
max_repeat = min(max_repeat, max_length)
|
|
106
|
+
return min_repeat, max_repeat
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _strip_quantifier(pattern: str) -> str:
|
|
110
|
+
"""Remove quantifier from the pattern."""
|
|
111
|
+
# Lazy & posessive quantifiers
|
|
112
|
+
if pattern.endswith(("*?", "+?", "??", "*+", "?+", "++")):
|
|
113
|
+
return pattern[:-2]
|
|
114
|
+
if pattern.endswith(("?", "*", "+")):
|
|
115
|
+
pattern = pattern[:-1]
|
|
116
|
+
if pattern.endswith("}"):
|
|
117
|
+
# Find the start of the exact quantifier and drop everything since that index
|
|
118
|
+
idx = pattern.rfind("{")
|
|
119
|
+
pattern = pattern[:idx]
|
|
120
|
+
return pattern
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: schemathesis
|
|
3
|
-
Version: 3.36.
|
|
3
|
+
Version: 3.36.3
|
|
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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
schemathesis/__init__.py,sha256=UW2Bq8hDDkcBeAAA7PzpBFXkOOxkmHox-mfQwzHDjL0,1914
|
|
2
2
|
schemathesis/_compat.py,sha256=y4RZd59i2NCnZ91VQhnKeMn_8t3SgvLOk2Xm8nymUHY,1837
|
|
3
3
|
schemathesis/_dependency_versions.py,sha256=pjEkkGAfOQJYNb-9UOo84V8nj_lKHr_TGDVdFwY2UU0,816
|
|
4
|
-
schemathesis/_hypothesis.py,sha256=
|
|
4
|
+
schemathesis/_hypothesis.py,sha256=TUGODyKJzTSnUQPWt_uj6-MBXCPthquJXGiCobVdFh0,14930
|
|
5
5
|
schemathesis/_lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
|
6
6
|
schemathesis/_override.py,sha256=TAjYB3eJQmlw9K_xiR9ptt9Wj7if4U7UFlUhGjpBAoM,1625
|
|
7
7
|
schemathesis/_rate_limiter.py,sha256=q_XWst5hzuAyXQRiZc4s_bx7-JlPYZM_yKDmeavt3oo,242
|
|
@@ -17,7 +17,7 @@ schemathesis/graphql.py,sha256=XiuKcfoOB92iLFC8zpz2msLkM0_V0TLdxPNBqrrGZ8w,216
|
|
|
17
17
|
schemathesis/hooks.py,sha256=qXyVRfJdhsLk1GuJX47VAqkX0VPm6X6fK-cXhEnFLT4,14765
|
|
18
18
|
schemathesis/lazy.py,sha256=uE8ef_7U_9ovs0-7UA7ssIiiDipJurJFHuxaUFOUETo,18956
|
|
19
19
|
schemathesis/loaders.py,sha256=MoEhcdOEBJxNRn5X-ZNhWB9jZDHQQNpkNfEdQjf_NDw,4590
|
|
20
|
-
schemathesis/models.py,sha256=
|
|
20
|
+
schemathesis/models.py,sha256=mgufFwK6pVWn6hjfJIJwbk0qZM-XUeUaVVaOOEPaR50,46646
|
|
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=Lycn1VVfula9B6XpzkxTHja7CZ7RHqbUh9kBic0Yi4M,9056
|
|
@@ -30,7 +30,7 @@ schemathesis/utils.py,sha256=8RkTZ9Ft5IUaGkxABhh34oU7WO2ouMsfgtvFPTx9alI,4875
|
|
|
30
30
|
schemathesis/cli/__init__.py,sha256=OC6QO38QDf55DTIVwrWiQKz8BfTD5QcK574m67NCE2w,72862
|
|
31
31
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
|
32
32
|
schemathesis/cli/callbacks.py,sha256=grMKlx_iGiJA4oJsYt_q8l354Y8Nb11IBvOKwbD0jOA,15192
|
|
33
|
-
schemathesis/cli/cassettes.py,sha256=
|
|
33
|
+
schemathesis/cli/cassettes.py,sha256=uzU51QMqUWhkxdX4H9bavVjeTIWX5_i-E3CnOK2mAU4,19698
|
|
34
34
|
schemathesis/cli/constants.py,sha256=wk-0GsoJIel8wFFerQ6Kf_6eAYUtIWkwMFwyAqv3yj4,1635
|
|
35
35
|
schemathesis/cli/context.py,sha256=j_lvYQiPa6Q7P4P_IGCM9V2y2gJSpDbpxIIzR5oFB2I,2567
|
|
36
36
|
schemathesis/cli/debug.py,sha256=_YA-bX1ujHl4bqQDEum7M-I2XHBTEGbvgkhvcvKhmgU,658
|
|
@@ -60,9 +60,9 @@ schemathesis/fixups/utf8_bom.py,sha256=lWT9RNmJG8i-l5AXIpaCT3qCPUwRgzXPW3eoOjmZE
|
|
|
60
60
|
schemathesis/generation/__init__.py,sha256=29Zys_tD6kfngaC4zHeC6TOBZQcmo7CWm7KDSYsHStQ,1581
|
|
61
61
|
schemathesis/generation/_hypothesis.py,sha256=QDBzpcM9eXPgLGGdCPdGlxCtfMXD4YBN9_6Oz73lofI,1406
|
|
62
62
|
schemathesis/generation/_methods.py,sha256=jCK09f4sedDfePrS-6BIiE-CcEE8fJ4ZHxq1BHoTltQ,1101
|
|
63
|
-
schemathesis/generation/coverage.py,sha256=
|
|
63
|
+
schemathesis/generation/coverage.py,sha256=5NyDjceKqmQVmDn4xwOnS61xRpS2Z_YcbtjNuveI0RA,27582
|
|
64
64
|
schemathesis/internal/__init__.py,sha256=93HcdG3LF0BbQKbCteOsFMa1w6nXl8yTmx87QLNJOik,161
|
|
65
|
-
schemathesis/internal/checks.py,sha256=
|
|
65
|
+
schemathesis/internal/checks.py,sha256=m6lY6x2Pkz6AjU8Hs-UMSDJZEOq8EcgZOCp6BQA7p_g,1668
|
|
66
66
|
schemathesis/internal/copy.py,sha256=DcL56z-d69kKR_5u8mlHvjSL1UTyUKNMAwexrwHFY1s,1031
|
|
67
67
|
schemathesis/internal/datetime.py,sha256=zPLBL0XXLNfP-KYel3H2m8pnsxjsA_4d-zTOhJg2EPQ,136
|
|
68
68
|
schemathesis/internal/deprecation.py,sha256=Ty5VBFBlufkITpP0WWTPIPbnB7biDi0kQgXVYWZp820,1273
|
|
@@ -75,7 +75,7 @@ schemathesis/internal/validation.py,sha256=G7i8jIMUpAeOnDsDF_eWYvRZe_yMprRswx0QA
|
|
|
75
75
|
schemathesis/runner/__init__.py,sha256=dLvb4FvH1zvYyVj5i7naR0ehfKL7K8QBKKbBNp_ClY8,21536
|
|
76
76
|
schemathesis/runner/events.py,sha256=cRKKSDvHvKLBIyFBz-J0JtAKshbGGKco9eaMyLCgzsY,11734
|
|
77
77
|
schemathesis/runner/probes.py,sha256=no5AfO3kse25qvHevjeUfB0Q3C860V2AYzschUW3QMQ,5688
|
|
78
|
-
schemathesis/runner/serialization.py,sha256=
|
|
78
|
+
schemathesis/runner/serialization.py,sha256=jHpfm1PgPAmorNkF8_rkzIYoeA43jpbSKeh5Hm5nqF0,20495
|
|
79
79
|
schemathesis/runner/impl/__init__.py,sha256=1E2iME8uthYPBh9MjwVBCTFV-P3fi7AdphCCoBBspjs,199
|
|
80
80
|
schemathesis/runner/impl/context.py,sha256=KT3Dl1HIUM29Jpp_DwfoSx_NbWFH_7s6gw-p2Sr-N24,2505
|
|
81
81
|
schemathesis/runner/impl/core.py,sha256=bAPwfhLJrXLlPN6BDNGyQoO9v35YlmeuxAmyFTNQg5Y,47197
|
|
@@ -104,10 +104,10 @@ schemathesis/specs/graphql/schemas.py,sha256=b7QwglKbcYQCMjuYmqDsVoFu2o4xaA_kduU
|
|
|
104
104
|
schemathesis/specs/graphql/validation.py,sha256=uINIOt-2E7ZuQV2CxKzwez-7L9tDtqzMSpnVoRWvxy0,1635
|
|
105
105
|
schemathesis/specs/openapi/__init__.py,sha256=HDcx3bqpa6qWPpyMrxAbM3uTo0Lqpg-BUNZhDJSJKnw,279
|
|
106
106
|
schemathesis/specs/openapi/_cache.py,sha256=PAiAu4X_a2PQgD2lG5H3iisXdyg4SaHpU46bRZvfNkM,4320
|
|
107
|
-
schemathesis/specs/openapi/_hypothesis.py,sha256=
|
|
108
|
-
schemathesis/specs/openapi/checks.py,sha256
|
|
107
|
+
schemathesis/specs/openapi/_hypothesis.py,sha256=Ym1d3GXlabOSbDk_AEkmkZGl9EMIDpumciLZtfYNdUI,24323
|
|
108
|
+
schemathesis/specs/openapi/checks.py,sha256=-4qOzkova0e4QSqdgsoUiOv2bg57HZmzbpAiAeotc3Q,22288
|
|
109
109
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
|
110
|
-
schemathesis/specs/openapi/converter.py,sha256=
|
|
110
|
+
schemathesis/specs/openapi/converter.py,sha256=NkrzBNjtmVwQTeE73NOtwB_puvQTjxxqqrc7gD_yscc,3241
|
|
111
111
|
schemathesis/specs/openapi/definitions.py,sha256=nEsCKn_LgqYjZ9nNWp-8KUIrB4S94pT3GsV5A8UIzDw,94043
|
|
112
112
|
schemathesis/specs/openapi/examples.py,sha256=FwhPWca7bpdHpUp_LRoK09DVgusojO3aXXhXYrK373I,20354
|
|
113
113
|
schemathesis/specs/openapi/formats.py,sha256=JmmkQWNAj5XreXb7Edgj4LADAf4m86YulR_Ec8evpJ4,1220
|
|
@@ -115,6 +115,7 @@ schemathesis/specs/openapi/links.py,sha256=a8JmWM9aZhrR5CfyIh6t2SkfonMLfYKOScXY2
|
|
|
115
115
|
schemathesis/specs/openapi/loaders.py,sha256=5B1cgYEBj3h2psPQxzrQ5Xq5owLVGw-u9HsCQIx7yFE,25705
|
|
116
116
|
schemathesis/specs/openapi/media_types.py,sha256=dNTxpRQbY3SubdVjh4Cjb38R6Bc9MF9BsRQwPD87x0g,1017
|
|
117
117
|
schemathesis/specs/openapi/parameters.py,sha256=CqJdS4d14l25_yEbqkLCnfIdDTlodRhJpxD8EXdaFwM,14059
|
|
118
|
+
schemathesis/specs/openapi/patterns.py,sha256=IK2BkXI1xByEz5if6jvydFE07nq5rDa4k_-2xX7ifG8,4715
|
|
118
119
|
schemathesis/specs/openapi/references.py,sha256=euxM02kQGMHh4Ss1jWjOY_gyw_HazafKITIsvOEiAvI,9831
|
|
119
120
|
schemathesis/specs/openapi/schemas.py,sha256=t3Gz2q-d9b8Oy-hDhz0rNfjYT3Nx-uOeLOmjO9hpRM0,53741
|
|
120
121
|
schemathesis/specs/openapi/security.py,sha256=Z-6pk2Ga1PTUtBe298KunjVHsNh5A-teegeso7zcPIE,7138
|
|
@@ -150,8 +151,8 @@ schemathesis/transports/auth.py,sha256=urSTO9zgFO1qU69xvnKHPFQV0SlJL3d7_Ojl0tLnZ
|
|
|
150
151
|
schemathesis/transports/content_types.py,sha256=MiKOm-Hy5i75hrROPdpiBZPOTDzOwlCdnthJD12AJzI,2187
|
|
151
152
|
schemathesis/transports/headers.py,sha256=hr_AIDOfUxsJxpHfemIZ_uNG3_vzS_ZeMEKmZjbYiBE,990
|
|
152
153
|
schemathesis/transports/responses.py,sha256=OFD4ZLqwEFpo7F9vaP_SVgjhxAqatxIj38FS4XVq8Qs,1680
|
|
153
|
-
schemathesis-3.36.
|
|
154
|
-
schemathesis-3.36.
|
|
155
|
-
schemathesis-3.36.
|
|
156
|
-
schemathesis-3.36.
|
|
157
|
-
schemathesis-3.36.
|
|
154
|
+
schemathesis-3.36.3.dist-info/METADATA,sha256=7TD5mpHwI_7wHLTg2EfyokGGn12X5d-gT6Wfc08-iEI,12904
|
|
155
|
+
schemathesis-3.36.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
156
|
+
schemathesis-3.36.3.dist-info/entry_points.txt,sha256=VHyLcOG7co0nOeuk8WjgpRETk5P1E2iCLrn26Zkn5uk,158
|
|
157
|
+
schemathesis-3.36.3.dist-info/licenses/LICENSE,sha256=PsPYgrDhZ7g9uwihJXNG-XVb55wj2uYhkl2DD8oAzY0,1103
|
|
158
|
+
schemathesis-3.36.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|