schemathesis 4.3.15__py3-none-any.whl → 4.3.17__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.

Potentially problematic release.


This version of schemathesis might be problematic. Click here for more details.

@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from itertools import chain
6
6
  from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, Sequence, cast
7
7
 
8
+ from schemathesis.config import GenerationConfig
8
9
  from schemathesis.core import NOT_SET, NotSet
9
10
  from schemathesis.core.adapter import OperationParameter
10
11
  from schemathesis.core.errors import InvalidSchema
@@ -13,12 +14,16 @@ from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
13
14
  from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject
14
15
  from schemathesis.core.parameters import HEADER_LOCATIONS, ParameterLocation
15
16
  from schemathesis.core.validation import check_header_name
16
- from schemathesis.schemas import ParameterSet
17
+ from schemathesis.generation.modes import GenerationMode
18
+ from schemathesis.schemas import APIOperation, ParameterSet
17
19
  from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
18
20
  from schemathesis.specs.openapi.adapter.references import maybe_resolve
19
21
  from schemathesis.specs.openapi.converter import to_json_schema
22
+ from schemathesis.specs.openapi.formats import HEADER_FORMAT
20
23
 
21
24
  if TYPE_CHECKING:
25
+ from hypothesis import strategies as st
26
+
22
27
  from schemathesis.core.compat import RefResolver
23
28
 
24
29
 
@@ -191,6 +196,8 @@ class OpenApiBody(OpenApiComponent):
191
196
  "_unoptimized_schema",
192
197
  "_raw_schema",
193
198
  "_examples",
199
+ "_positive_strategy_cache",
200
+ "_negative_strategy_cache",
194
201
  )
195
202
 
196
203
  @classmethod
@@ -231,6 +238,11 @@ class OpenApiBody(OpenApiComponent):
231
238
  adapter=adapter,
232
239
  )
233
240
 
241
+ def __post_init__(self) -> None:
242
+ super().__post_init__()
243
+ self._positive_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
244
+ self._negative_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
245
+
234
246
  @property
235
247
  def location(self) -> ParameterLocation:
236
248
  return ParameterLocation.BODY
@@ -248,6 +260,47 @@ class OpenApiBody(OpenApiComponent):
248
260
  """Return default type if body is a form type."""
249
261
  return "object" if self.media_type in FORM_MEDIA_TYPES else None
250
262
 
263
+ def get_strategy(
264
+ self,
265
+ operation: APIOperation,
266
+ generation_config: GenerationConfig,
267
+ generation_mode: GenerationMode,
268
+ ) -> st.SearchStrategy:
269
+ """Get a Hypothesis strategy for this body parameter."""
270
+ # Check cache based on generation mode
271
+ if generation_mode == GenerationMode.POSITIVE:
272
+ if self._positive_strategy_cache is not NOT_SET:
273
+ assert not isinstance(self._positive_strategy_cache, NotSet)
274
+ return self._positive_strategy_cache
275
+ elif self._negative_strategy_cache is not NOT_SET:
276
+ assert not isinstance(self._negative_strategy_cache, NotSet)
277
+ return self._negative_strategy_cache
278
+
279
+ # Import here to avoid circular dependency
280
+ from schemathesis.specs.openapi._hypothesis import GENERATOR_MODE_TO_STRATEGY_FACTORY
281
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
282
+
283
+ # Build the strategy
284
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
285
+ schema = self.optimized_schema
286
+ assert isinstance(operation.schema, BaseOpenAPISchema)
287
+ strategy = strategy_factory(
288
+ schema,
289
+ operation.label,
290
+ ParameterLocation.BODY,
291
+ self.media_type,
292
+ generation_config,
293
+ operation.schema.adapter.jsonschema_validator_cls,
294
+ )
295
+
296
+ # Cache the strategy
297
+ if generation_mode == GenerationMode.POSITIVE:
298
+ self._positive_strategy_cache = strategy
299
+ else:
300
+ self._negative_strategy_cache = strategy
301
+
302
+ return strategy
303
+
251
304
 
252
305
  OPENAPI_20_EXCLUDE_KEYS = frozenset(["required", "name", "in", "title", "description"])
253
306
 
@@ -461,20 +514,123 @@ def build_path_parameter_v3_1(kwargs: Mapping[str, Any]) -> OpenApiParameter:
461
514
  @dataclass
462
515
  class OpenApiParameterSet(ParameterSet):
463
516
  items: list[OpenApiParameter]
517
+ location: ParameterLocation
464
518
 
465
- __slots__ = ("items", "_schema")
519
+ __slots__ = ("items", "location", "_schema", "_schema_cache", "_strategy_cache")
466
520
 
467
- def __init__(self, items: list[OpenApiParameter] | None = None) -> None:
521
+ def __init__(self, location: ParameterLocation, items: list[OpenApiParameter] | None = None) -> None:
522
+ self.location = location
468
523
  self.items = items or []
469
524
  self._schema: dict | NotSet = NOT_SET
525
+ self._schema_cache: dict[frozenset[str], dict[str, Any]] = {}
526
+ self._strategy_cache: dict[tuple[frozenset[str], GenerationMode], st.SearchStrategy] = {}
470
527
 
471
528
  @property
472
529
  def schema(self) -> dict[str, Any]:
473
530
  if self._schema is NOT_SET:
474
- self._schema = parameters_to_json_schema(self.items)
531
+ self._schema = parameters_to_json_schema(self.items, self.location)
475
532
  assert not isinstance(self._schema, NotSet)
476
533
  return self._schema
477
534
 
535
+ def get_schema_with_exclusions(self, exclude: Iterable[str]) -> dict[str, Any]:
536
+ """Get cached schema with specified parameters excluded."""
537
+ exclude_key = frozenset(exclude)
538
+
539
+ if exclude_key in self._schema_cache:
540
+ return self._schema_cache[exclude_key]
541
+
542
+ schema = self.schema
543
+ if exclude_key:
544
+ # Need to exclude some parameters - create a shallow copy to avoid mutating cached schema
545
+ schema = dict(schema)
546
+ if self.location == ParameterLocation.HEADER:
547
+ # Remove excluded headers case-insensitively
548
+ exclude_lower = {name.lower() for name in exclude_key}
549
+ schema["properties"] = {
550
+ key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
551
+ }
552
+ if "required" in schema:
553
+ schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
554
+ else:
555
+ # Non-header locations: remove by exact name
556
+ schema["properties"] = {
557
+ key: value for key, value in schema["properties"].items() if key not in exclude_key
558
+ }
559
+ if "required" in schema:
560
+ schema["required"] = [key for key in schema["required"] if key not in exclude_key]
561
+
562
+ self._schema_cache[exclude_key] = schema
563
+ return schema
564
+
565
+ def get_strategy(
566
+ self,
567
+ operation: APIOperation,
568
+ generation_config: GenerationConfig,
569
+ generation_mode: GenerationMode,
570
+ exclude: Iterable[str] = (),
571
+ ) -> st.SearchStrategy:
572
+ """Get a Hypothesis strategy for this parameter set with specified exclusions."""
573
+ exclude_key = frozenset(exclude)
574
+ cache_key = (exclude_key, generation_mode)
575
+
576
+ if cache_key in self._strategy_cache:
577
+ return self._strategy_cache[cache_key]
578
+
579
+ # Import here to avoid circular dependency
580
+ from hypothesis import strategies as st
581
+
582
+ from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query
583
+ from schemathesis.specs.openapi._hypothesis import (
584
+ GENERATOR_MODE_TO_STRATEGY_FACTORY,
585
+ _can_skip_header_filter,
586
+ jsonify_python_specific_types,
587
+ make_negative_strategy,
588
+ quote_all,
589
+ )
590
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
591
+
592
+ # Get schema with exclusions
593
+ schema = self.get_schema_with_exclusions(exclude)
594
+
595
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
596
+
597
+ if not schema["properties"] and strategy_factory is make_negative_strategy:
598
+ # Nothing to negate - all properties were excluded
599
+ strategy = st.none()
600
+ else:
601
+ assert isinstance(operation.schema, BaseOpenAPISchema)
602
+ strategy = strategy_factory(
603
+ schema,
604
+ operation.label,
605
+ self.location,
606
+ None,
607
+ generation_config,
608
+ operation.schema.adapter.jsonschema_validator_cls,
609
+ )
610
+ serialize = operation.get_parameter_serializer(self.location)
611
+ if serialize is not None:
612
+ strategy = strategy.map(serialize)
613
+ filter_func = {
614
+ ParameterLocation.PATH: is_valid_path,
615
+ ParameterLocation.HEADER: is_valid_header,
616
+ ParameterLocation.COOKIE: is_valid_header,
617
+ ParameterLocation.QUERY: is_valid_query,
618
+ }[self.location]
619
+ # Headers with special format do not need filtration
620
+ if not (self.location.is_in_header and _can_skip_header_filter(schema)):
621
+ strategy = strategy.filter(filter_func)
622
+ # Path & query parameters will be cast to string anyway, but having their JSON equivalents for
623
+ # `True` / `False` / `None` improves chances of them passing validation in apps
624
+ # that expect boolean / null types
625
+ # and not aware of Python-specific representation of those types
626
+ if self.location == ParameterLocation.PATH:
627
+ strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
628
+ elif self.location == ParameterLocation.QUERY:
629
+ strategy = strategy.map(jsonify_python_specific_types)
630
+
631
+ self._strategy_cache[cache_key] = strategy
632
+ return strategy
633
+
478
634
 
479
635
  COMBINED_FORM_DATA_MARKER = "x-schemathesis-form-parameter"
480
636
 
@@ -485,19 +641,21 @@ def form_data_to_json_schema(parameters: Sequence[Mapping[str, Any]]) -> dict[st
485
641
  (param["name"], extract_parameter_schema_v2(param), param.get("required", False)) for param in parameters
486
642
  )
487
643
 
488
- merged = _merge_parameters_to_object_schema(parameter_data)
644
+ merged = _merge_parameters_to_object_schema(parameter_data, ParameterLocation.BODY)
489
645
 
490
646
  return {"schema": merged, COMBINED_FORM_DATA_MARKER: True}
491
647
 
492
648
 
493
- def parameters_to_json_schema(parameters: Iterable[OpenApiParameter]) -> dict[str, Any]:
649
+ def parameters_to_json_schema(parameters: Iterable[OpenApiParameter], location: ParameterLocation) -> dict[str, Any]:
494
650
  """Convert multiple Open API parameters to a JSON Schema."""
495
651
  parameter_data = ((param.name, param.optimized_schema, param.is_required) for param in parameters)
496
652
 
497
- return _merge_parameters_to_object_schema(parameter_data)
653
+ return _merge_parameters_to_object_schema(parameter_data, location)
498
654
 
499
655
 
500
- def _merge_parameters_to_object_schema(parameters: Iterable[tuple[str, Any, bool]]) -> dict[str, Any]:
656
+ def _merge_parameters_to_object_schema(
657
+ parameters: Iterable[tuple[str, Any, bool]], location: ParameterLocation
658
+ ) -> dict[str, Any]:
501
659
  """Merge parameter data into a JSON Schema object."""
502
660
  properties = {}
503
661
  required = []
@@ -512,18 +670,30 @@ def _merge_parameters_to_object_schema(parameters: Iterable[tuple[str, Any, bool
512
670
  # ensures unique names
513
671
  bundled.update(subschema_bundle)
514
672
 
673
+ # Apply location-specific adjustments to individual parameter schemas
674
+ if isinstance(subschema, dict):
675
+ # Headers: add HEADER_FORMAT for plain string types
676
+ if location.is_in_header and list(subschema) == ["type"] and subschema["type"] == "string":
677
+ subschema = {**subschema, "format": HEADER_FORMAT}
678
+
679
+ # Path parameters: ensure string types have minLength >= 1
680
+ elif location == ParameterLocation.PATH and subschema.get("type") == "string":
681
+ if "minLength" not in subschema:
682
+ subschema = {**subschema, "minLength": 1}
683
+
515
684
  properties[name] = subschema
516
685
 
517
- # Avoid duplicate entries in required
518
- if is_required and name not in required:
686
+ # Path parameters are always required
687
+ if (location == ParameterLocation.PATH or is_required) and name not in required:
519
688
  required.append(name)
520
689
 
521
690
  merged = {
522
691
  "properties": properties,
523
692
  "additionalProperties": False,
524
693
  "type": "object",
525
- "required": required,
526
694
  }
695
+ if required:
696
+ merged["required"] = required
527
697
  if bundled:
528
698
  merged[BUNDLE_STORAGE_KEY] = bundled
529
699
 
@@ -14,7 +14,7 @@ from schemathesis.core.failures import Failure
14
14
  from schemathesis.core.parameters import ParameterLocation
15
15
  from schemathesis.core.transport import Response
16
16
  from schemathesis.generation.case import Case
17
- from schemathesis.generation.meta import CoveragePhaseData, TestPhase
17
+ from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario, TestPhase
18
18
  from schemathesis.openapi.checks import (
19
19
  AcceptedNegativeData,
20
20
  EnsureResourceAvailability,
@@ -44,7 +44,7 @@ def is_unexpected_http_status_case(case: Case) -> bool:
44
44
  return bool(
45
45
  case.meta
46
46
  and isinstance(case.meta.phase.data, CoveragePhaseData)
47
- and case.meta.phase.data.description.startswith("Unspecified HTTP method")
47
+ and case.meta.phase.data.scenario == CoverageScenario.UNSPECIFIED_HTTP_METHOD
48
48
  )
49
49
 
50
50
 
@@ -287,8 +287,7 @@ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -
287
287
  if (
288
288
  data.parameter
289
289
  and data.parameter_location == ParameterLocation.HEADER
290
- and data.description
291
- and data.description.startswith("Missing ")
290
+ and data.scenario == CoverageScenario.MISSING_PARAMETER
292
291
  ):
293
292
  if data.parameter.lower() == "authorization":
294
293
  expected_statuses = {401}
@@ -313,7 +312,7 @@ def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> boo
313
312
  if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or response.request.method == "OPTIONS":
314
313
  return None
315
314
  data = meta.phase.data
316
- if data.description and data.description.startswith("Unspecified HTTP method:"):
315
+ if data.scenario == CoverageScenario.UNSPECIFIED_HTTP_METHOD:
317
316
  if response.status_code != 405:
318
317
  raise UnsupportedMethodResponse(
319
318
  operation=case.operation.label,
@@ -340,7 +339,6 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
340
339
  # Check if the case contains only additional properties in query, headers, or cookies.
341
340
  # This function is used to determine if negation is solely in the form of extra properties,
342
341
  # which are often ignored for backward-compatibility by the tested apps
343
- from ._hypothesis import get_schema_for_location
344
342
  from .schemas import BaseOpenAPISchema
345
343
 
346
344
  meta = case.meta
@@ -358,7 +356,7 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
358
356
  value = getattr(case, location.container_name)
359
357
  if value is not None and meta_for_location is not None and meta_for_location.mode.is_negative:
360
358
  container = getattr(case.operation, location.container_name)
361
- schema = get_schema_for_location(location, container)
359
+ schema = container.schema
362
360
 
363
361
  if _has_serialization_sensitive_types(schema, container):
364
362
  # Can't reliably determine if only additional properties were added
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from itertools import chain
4
3
  from typing import Any, Callable, overload
5
4
 
6
5
  from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
@@ -188,29 +187,17 @@ def update_pattern_in_schema(schema: dict[str, Any]) -> None:
188
187
 
189
188
  def rewrite_properties(schema: dict[str, Any], predicate: Callable[[dict[str, Any]], bool]) -> None:
190
189
  required = schema.get("required", [])
191
- forbidden = []
192
190
  for name, subschema in list(schema.get("properties", {}).items()):
193
191
  if predicate(subschema):
194
192
  if name in required:
195
193
  required.remove(name)
196
- del schema["properties"][name]
197
- forbidden.append(name)
198
- if forbidden:
199
- forbid_properties(schema, forbidden)
194
+ schema["properties"][name] = {"not": {}}
200
195
  if not schema.get("required"):
201
196
  schema.pop("required", None)
202
197
  if not schema.get("properties"):
203
198
  schema.pop("properties", None)
204
199
 
205
200
 
206
- def forbid_properties(schema: dict[str, Any], forbidden: list[str]) -> None:
207
- """Explicitly forbid properties via the `not` keyword."""
208
- not_schema = schema.setdefault("not", {})
209
- already_forbidden = not_schema.setdefault("required", [])
210
- already_forbidden.extend(forbidden)
211
- not_schema["required"] = list(set(chain(already_forbidden, forbidden)))
212
-
213
-
214
201
  def is_write_only(schema: dict[str, Any] | bool) -> bool:
215
202
  if isinstance(schema, bool):
216
203
  return False
@@ -242,7 +242,7 @@ def _expand_subschemas(
242
242
  except InfiniteRecursiveReference:
243
243
  return
244
244
 
245
- yield (schema, current_path)
245
+ yield schema, current_path
246
246
 
247
247
  if isinstance(schema, dict):
248
248
  # For anyOf/oneOf, yield each alternative with the same path
@@ -250,10 +250,10 @@ def _expand_subschemas(
250
250
  if key in schema:
251
251
  for subschema in schema[key]:
252
252
  # Each alternative starts with the current path
253
- yield (subschema, current_path)
253
+ yield subschema, current_path
254
254
 
255
255
  # For allOf, merge all alternatives
256
- if "allOf" in schema and schema["allOf"]:
256
+ if schema.get("allOf"):
257
257
  subschema = deepclone(schema["allOf"][0])
258
258
  try:
259
259
  subschema, expanded_path = _resolve_bundled(subschema, resolver, current_path)
@@ -278,7 +278,7 @@ def _expand_subschemas(
278
278
  else:
279
279
  subschema[key] = value
280
280
 
281
- yield (subschema, expanded_path)
281
+ yield subschema, expanded_path
282
282
 
283
283
 
284
284
  def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
@@ -9,6 +9,7 @@ import requests
9
9
 
10
10
  from schemathesis.core.compat import RefResolutionError, RefResolver
11
11
  from schemathesis.core.deserialization import deserialize_yaml
12
+ from schemathesis.core.errors import RemoteDocumentError
12
13
  from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
13
14
 
14
15
 
@@ -30,10 +31,39 @@ def load_file_uri(location: str) -> dict[str, Any]:
30
31
  return load_file_impl(location, urlopen)
31
32
 
32
33
 
34
+ _HTML_MARKERS = (b"<!doctype", b"<html", b"<head", b"<body")
35
+
36
+
37
+ def _looks_like_html(content_type: str | None, body: bytes) -> bool:
38
+ if content_type and "html" in content_type.lower():
39
+ return True
40
+ head = body.lstrip()[:64].lower()
41
+ return any(head.startswith(m) for m in _HTML_MARKERS)
42
+
43
+
33
44
  def load_remote_uri(uri: str) -> Any:
34
45
  """Load the resource and parse it as YAML / JSON."""
35
46
  response = requests.get(uri, timeout=DEFAULT_RESPONSE_TIMEOUT)
36
- return deserialize_yaml(response.content)
47
+ content_type = response.headers.get("Content-Type", "")
48
+ body = response.content or b""
49
+
50
+ def _suffix() -> str:
51
+ return f"(HTTP {response.status_code}, Content-Type={content_type}, size={len(body)})"
52
+
53
+ if not (200 <= response.status_code < 300):
54
+ raise RemoteDocumentError(f"Failed to fetch {_suffix()}")
55
+
56
+ if _looks_like_html(content_type, body):
57
+ raise RemoteDocumentError(f"Expected YAML/JSON, got HTML {_suffix()}")
58
+
59
+ document = deserialize_yaml(response.content)
60
+
61
+ if not isinstance(document, (dict, list)):
62
+ raise RemoteDocumentError(
63
+ f"Remote document is parsed as {type(document).__name__}, but an object/array is expected {_suffix()}"
64
+ )
65
+
66
+ return document
37
67
 
38
68
 
39
69
  JSONType = Union[None, bool, float, str, list, Dict[str, Any]]
@@ -35,6 +35,7 @@ from schemathesis.core.errors import (
35
35
  SchemaLocation,
36
36
  )
37
37
  from schemathesis.core.failures import Failure, FailureGroup, MalformedJson
38
+ from schemathesis.core.parameters import ParameterLocation
38
39
  from schemathesis.core.result import Err, Ok, Result
39
40
  from schemathesis.core.transport import Response
40
41
  from schemathesis.generation.case import Case
@@ -389,10 +390,10 @@ class BaseOpenAPISchema(BaseSchema):
389
390
  schema=self,
390
391
  responses=responses,
391
392
  security=security,
392
- path_parameters=OpenApiParameterSet(),
393
- query=OpenApiParameterSet(),
394
- headers=OpenApiParameterSet(),
395
- cookies=OpenApiParameterSet(),
393
+ path_parameters=OpenApiParameterSet(ParameterLocation.PATH),
394
+ query=OpenApiParameterSet(ParameterLocation.QUERY),
395
+ headers=OpenApiParameterSet(ParameterLocation.HEADER),
396
+ cookies=OpenApiParameterSet(ParameterLocation.COOKIE),
396
397
  )
397
398
  for parameter in parameters:
398
399
  operation.add_parameter(parameter)
@@ -8,8 +8,9 @@ from schemathesis.config import SanitizationConfig
8
8
  from schemathesis.core import SCHEMATHESIS_TEST_CASE_HEADER, NotSet
9
9
  from schemathesis.core.errors import InvalidSchema
10
10
  from schemathesis.core.output.sanitization import sanitize_url, sanitize_value
11
+ from schemathesis.core.parameters import ParameterLocation
11
12
  from schemathesis.core.transport import USER_AGENT
12
- from schemathesis.generation.meta import CoveragePhaseData
13
+ from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from requests import PreparedRequest
@@ -41,8 +42,8 @@ def get_exclude_headers(case: Case) -> list[str]:
41
42
  if (
42
43
  case.meta is not None
43
44
  and isinstance(case.meta.phase.data, CoveragePhaseData)
44
- and case.meta.phase.data.description.startswith("Missing")
45
- and case.meta.phase.data.description.endswith("at header")
45
+ and case.meta.phase.data.scenario == CoverageScenario.MISSING_PARAMETER
46
+ and case.meta.phase.data.parameter_location == ParameterLocation.HEADER
46
47
  and case.meta.phase.data.parameter is not None
47
48
  ):
48
49
  return [case.meta.phase.data.parameter]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: schemathesis
3
- Version: 4.3.15
3
+ Version: 4.3.17
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://github.com/schemathesis/schemathesis/blob/master/CHANGELOG.md
@@ -25,6 +25,7 @@ Classifier: Programming Language :: Python :: 3.10
25
25
  Classifier: Programming Language :: Python :: 3.11
26
26
  Classifier: Programming Language :: Python :: 3.12
27
27
  Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
28
29
  Classifier: Programming Language :: Python :: Implementation :: CPython
29
30
  Classifier: Programming Language :: Python :: Implementation :: PyPy
30
31
  Classifier: Topic :: Software Development :: Testing
@@ -50,7 +51,7 @@ Requires-Dist: tomli>=2.2.1; python_version < '3.11'
50
51
  Requires-Dist: typing-extensions>=4.12.2
51
52
  Requires-Dist: werkzeug<4,>=0.16.0
52
53
  Provides-Extra: bench
53
- Requires-Dist: pytest-codspeed==2.2.1; extra == 'bench'
54
+ Requires-Dist: pytest-codspeed==4.2.0; extra == 'bench'
54
55
  Provides-Extra: cov
55
56
  Requires-Dist: coverage-enable-subprocess; extra == 'cov'
56
57
  Requires-Dist: coverage[toml]>=5.3; extra == 'cov'
@@ -65,8 +66,8 @@ Requires-Dist: hypothesis-openapi<1,>=0.2; (python_version >= '3.10') and extra
65
66
  Requires-Dist: mkdocs-material; extra == 'dev'
66
67
  Requires-Dist: mkdocstrings[python]; extra == 'dev'
67
68
  Requires-Dist: pydantic>=1.10.2; extra == 'dev'
68
- Requires-Dist: pytest-asyncio<1.0,>=0.18.0; extra == 'dev'
69
- Requires-Dist: pytest-codspeed==2.2.1; extra == 'dev'
69
+ Requires-Dist: pytest-asyncio<2.0,>=1.0; extra == 'dev'
70
+ Requires-Dist: pytest-codspeed==4.2.0; extra == 'dev'
70
71
  Requires-Dist: pytest-httpserver<2.0,>=1.0; extra == 'dev'
71
72
  Requires-Dist: pytest-mock<4.0,>=3.7.0; extra == 'dev'
72
73
  Requires-Dist: pytest-trio<1.0,>=0.8; extra == 'dev'
@@ -85,7 +86,7 @@ Requires-Dist: fastapi>=0.86.0; extra == 'tests'
85
86
  Requires-Dist: flask<3.0,>=2.1.1; extra == 'tests'
86
87
  Requires-Dist: hypothesis-openapi<1,>=0.2; (python_version >= '3.10') and extra == 'tests'
87
88
  Requires-Dist: pydantic>=1.10.2; extra == 'tests'
88
- Requires-Dist: pytest-asyncio<1.0,>=0.18.0; extra == 'tests'
89
+ Requires-Dist: pytest-asyncio<2.0,>=1.0; extra == 'tests'
89
90
  Requires-Dist: pytest-httpserver<2.0,>=1.0; extra == 'tests'
90
91
  Requires-Dist: pytest-mock<4.0,>=3.7.0; extra == 'tests'
91
92
  Requires-Dist: pytest-trio<1.0,>=0.8; extra == 'tests'