schemathesis 3.35.4__py3-none-any.whl → 3.36.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. schemathesis/__init__.py +5 -5
  2. schemathesis/_hypothesis.py +12 -6
  3. schemathesis/_override.py +4 -4
  4. schemathesis/auths.py +1 -1
  5. schemathesis/checks.py +8 -5
  6. schemathesis/cli/__init__.py +23 -26
  7. schemathesis/cli/callbacks.py +6 -4
  8. schemathesis/cli/cassettes.py +67 -41
  9. schemathesis/cli/context.py +7 -6
  10. schemathesis/cli/junitxml.py +1 -1
  11. schemathesis/cli/options.py +7 -4
  12. schemathesis/cli/output/default.py +5 -5
  13. schemathesis/cli/reporting.py +4 -2
  14. schemathesis/code_samples.py +4 -3
  15. schemathesis/contrib/unique_data.py +1 -2
  16. schemathesis/exceptions.py +4 -3
  17. schemathesis/extra/_flask.py +4 -1
  18. schemathesis/extra/pytest_plugin.py +6 -3
  19. schemathesis/failures.py +2 -1
  20. schemathesis/filters.py +2 -2
  21. schemathesis/generation/__init__.py +2 -2
  22. schemathesis/generation/_hypothesis.py +1 -1
  23. schemathesis/generation/coverage.py +53 -12
  24. schemathesis/graphql.py +0 -1
  25. schemathesis/hooks.py +3 -3
  26. schemathesis/internal/checks.py +53 -0
  27. schemathesis/lazy.py +10 -7
  28. schemathesis/loaders.py +3 -3
  29. schemathesis/models.py +59 -23
  30. schemathesis/runner/__init__.py +12 -6
  31. schemathesis/runner/events.py +1 -1
  32. schemathesis/runner/impl/context.py +72 -0
  33. schemathesis/runner/impl/core.py +105 -67
  34. schemathesis/runner/impl/solo.py +17 -20
  35. schemathesis/runner/impl/threadpool.py +65 -72
  36. schemathesis/runner/serialization.py +4 -3
  37. schemathesis/sanitization.py +2 -1
  38. schemathesis/schemas.py +20 -22
  39. schemathesis/serializers.py +2 -0
  40. schemathesis/service/client.py +1 -1
  41. schemathesis/service/events.py +4 -1
  42. schemathesis/service/extensions.py +2 -2
  43. schemathesis/service/hosts.py +4 -2
  44. schemathesis/service/models.py +3 -3
  45. schemathesis/service/report.py +3 -3
  46. schemathesis/service/serialization.py +4 -2
  47. schemathesis/specs/graphql/loaders.py +5 -4
  48. schemathesis/specs/graphql/schemas.py +13 -8
  49. schemathesis/specs/openapi/checks.py +76 -27
  50. schemathesis/specs/openapi/definitions.py +1 -5
  51. schemathesis/specs/openapi/examples.py +92 -2
  52. schemathesis/specs/openapi/expressions/__init__.py +7 -0
  53. schemathesis/specs/openapi/expressions/extractors.py +4 -1
  54. schemathesis/specs/openapi/expressions/nodes.py +5 -3
  55. schemathesis/specs/openapi/links.py +4 -4
  56. schemathesis/specs/openapi/loaders.py +6 -5
  57. schemathesis/specs/openapi/negative/__init__.py +5 -3
  58. schemathesis/specs/openapi/negative/mutations.py +5 -4
  59. schemathesis/specs/openapi/parameters.py +4 -2
  60. schemathesis/specs/openapi/schemas.py +28 -13
  61. schemathesis/specs/openapi/security.py +6 -4
  62. schemathesis/specs/openapi/stateful/__init__.py +2 -2
  63. schemathesis/specs/openapi/stateful/statistic.py +3 -3
  64. schemathesis/specs/openapi/stateful/types.py +3 -2
  65. schemathesis/stateful/__init__.py +3 -3
  66. schemathesis/stateful/config.py +2 -1
  67. schemathesis/stateful/context.py +13 -3
  68. schemathesis/stateful/events.py +3 -3
  69. schemathesis/stateful/runner.py +24 -6
  70. schemathesis/stateful/sink.py +1 -1
  71. schemathesis/stateful/state_machine.py +7 -6
  72. schemathesis/stateful/statistic.py +3 -1
  73. schemathesis/stateful/validation.py +10 -5
  74. schemathesis/transports/__init__.py +2 -2
  75. schemathesis/transports/asgi.py +7 -0
  76. schemathesis/transports/auth.py +2 -1
  77. schemathesis/transports/content_types.py +1 -1
  78. schemathesis/transports/responses.py +2 -1
  79. schemathesis/utils.py +4 -2
  80. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/METADATA +1 -1
  81. schemathesis-3.36.0.dist-info/RECORD +157 -0
  82. schemathesis-3.35.4.dist-info/RECORD +0 -154
  83. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/WHEEL +0 -0
  84. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/entry_points.txt +0 -0
  85. {schemathesis-3.35.4.dist-info → schemathesis-3.36.0.dist-info}/licenses/LICENSE +0 -0
@@ -22,14 +22,12 @@ from urllib.parse import urlsplit, urlunsplit
22
22
 
23
23
  import graphql
24
24
  from hypothesis import strategies as st
25
- from hypothesis.strategies import SearchStrategy
26
25
  from hypothesis_graphql import strategies as gql_st
27
26
  from requests.structures import CaseInsensitiveDict
28
27
 
29
28
  from ... import auths
30
- from ...auths import AuthStorage
31
29
  from ...checks import not_a_server_error
32
- from ...constants import NOT_SET
30
+ from ...constants import NOT_SET, SCHEMATHESIS_TEST_CASE_HEADER
33
31
  from ...exceptions import OperationNotFound, OperationSchemaError
34
32
  from ...generation import DataGenerationMethod, GenerationConfig
35
33
  from ...hooks import (
@@ -40,15 +38,19 @@ from ...hooks import (
40
38
  should_skip_operation,
41
39
  )
42
40
  from ...internal.result import Ok, Result
43
- from ...models import APIOperation, Case, CheckFunction, OperationDefinition
41
+ from ...models import APIOperation, Case, OperationDefinition
44
42
  from ...schemas import APIOperationMap, BaseSchema
45
- from ...stateful import Stateful, StatefulTest
46
43
  from ...types import Body, Cookies, Headers, NotSet, PathParameters, Query
47
44
  from ..openapi.constants import LOCATION_TO_CONTAINER
48
45
  from ._cache import OperationCache
49
46
  from .scalars import CUSTOM_SCALARS, get_extra_scalar_strategies
50
47
 
51
48
  if TYPE_CHECKING:
49
+ from hypothesis.strategies import SearchStrategy
50
+
51
+ from ...auths import AuthStorage
52
+ from ...internal.checks import CheckFunction
53
+ from ...stateful import Stateful, StatefulTest
52
54
  from ...transports.responses import GenericResponse
53
55
 
54
56
 
@@ -60,6 +62,9 @@ class RootType(enum.Enum):
60
62
 
61
63
  @dataclass(repr=False)
62
64
  class GraphQLCase(Case):
65
+ def __hash__(self) -> int:
66
+ return hash(self.as_curl_command({SCHEMATHESIS_TEST_CASE_HEADER: "0"}))
67
+
63
68
  def _get_url(self, base_url: str | None) -> str:
64
69
  base_url = self._get_base_url(base_url)
65
70
  # Replace the path, in case if the user provided any path parameters via hooks
@@ -77,11 +82,12 @@ class GraphQLCase(Case):
77
82
  additional_checks: tuple[CheckFunction, ...] = (),
78
83
  excluded_checks: tuple[CheckFunction, ...] = (),
79
84
  code_sample_style: str | None = None,
85
+ headers: dict[str, Any] | None = None,
80
86
  ) -> None:
81
87
  checks = checks or (not_a_server_error,)
82
88
  checks += additional_checks
83
89
  checks = tuple(check for check in checks if check not in excluded_checks)
84
- return super().validate_response(response, checks, code_sample_style=code_sample_style)
90
+ return super().validate_response(response, checks, code_sample_style=code_sample_style, headers=headers)
85
91
 
86
92
 
87
93
  C = TypeVar("C", bound=Case)
@@ -185,8 +191,7 @@ class GraphQLSchema(BaseSchema):
185
191
  return 0
186
192
 
187
193
  def get_all_operations(
188
- self,
189
- hooks: HookDispatcher | None = None,
194
+ self, hooks: HookDispatcher | None = None, generation_config: GenerationConfig | None = None
190
195
  ) -> Generator[Result[APIOperation, OperationSchemaError], None, None]:
191
196
  schema = self.client_schema
192
197
  for root_type, operation_type in (
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ import enum
4
5
  from http.cookies import SimpleCookie
5
6
  from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
6
7
  from urllib.parse import parse_qs, urlparse
@@ -25,11 +26,12 @@ from .utils import expand_status_code
25
26
  if TYPE_CHECKING:
26
27
  from requests import PreparedRequest
27
28
 
29
+ from ...internal.checks import CheckContext
28
30
  from ...models import APIOperation, Case
29
31
  from ...transports.responses import GenericResponse
30
32
 
31
33
 
32
- def status_code_conformance(response: GenericResponse, case: Case) -> bool | None:
34
+ def status_code_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
33
35
  from .schemas import BaseOpenAPISchema
34
36
 
35
37
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -60,7 +62,7 @@ def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, N
60
62
  yield from expand_status_code(code)
61
63
 
62
64
 
63
- def content_type_conformance(response: GenericResponse, case: Case) -> bool | None:
65
+ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
64
66
  from .schemas import BaseOpenAPISchema
65
67
 
66
68
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -115,7 +117,7 @@ def _reraise_malformed_media_type(case: Case, exc: ValueError, location: str, ac
115
117
  ) from exc
116
118
 
117
119
 
118
- def response_headers_conformance(response: GenericResponse, case: Case) -> bool | None:
120
+ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
119
121
  import jsonschema
120
122
 
121
123
  from .parameters import OpenAPI20Parameter, OpenAPI30Parameter
@@ -171,11 +173,11 @@ def response_headers_conformance(response: GenericResponse, case: Case) -> bool
171
173
  )
172
174
  except jsonschema.ValidationError as exc:
173
175
  exc_class = get_schema_validation_error(case.operation.verbose_name, exc)
174
- ctx = failures.ValidationErrorContext.from_exception(
176
+ error_ctx = failures.ValidationErrorContext.from_exception(
175
177
  exc, output_config=case.operation.schema.output_config
176
178
  )
177
179
  try:
178
- raise exc_class("Response header does not conform to the schema", context=ctx) from exc
180
+ raise exc_class("Response header does not conform to the schema", context=error_ctx) from exc
179
181
  except Exception as exc:
180
182
  errors.append(exc)
181
183
  return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
@@ -203,7 +205,7 @@ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | floa
203
205
  return value
204
206
 
205
207
 
206
- def response_schema_conformance(response: GenericResponse, case: Case) -> bool | None:
208
+ def response_schema_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
207
209
  from .schemas import BaseOpenAPISchema
208
210
 
209
211
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -211,7 +213,7 @@ def response_schema_conformance(response: GenericResponse, case: Case) -> bool |
211
213
  return case.operation.validate_response(response)
212
214
 
213
215
 
214
- def negative_data_rejection(response: GenericResponse, case: Case) -> bool | None:
216
+ def negative_data_rejection(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
215
217
  from .schemas import BaseOpenAPISchema
216
218
 
217
219
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -258,7 +260,7 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
258
260
  return True
259
261
 
260
262
 
261
- def use_after_free(response: GenericResponse, original: Case) -> bool | None:
263
+ def use_after_free(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
262
264
  from ...transports.responses import get_reason
263
265
  from .schemas import BaseOpenAPISchema
264
266
 
@@ -298,7 +300,7 @@ def use_after_free(response: GenericResponse, original: Case) -> bool | None:
298
300
  return None
299
301
 
300
302
 
301
- def ensure_resource_availability(response: GenericResponse, original: Case) -> bool | None:
303
+ def ensure_resource_availability(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
302
304
  from ...transports.responses import get_reason
303
305
  from .schemas import BaseOpenAPISchema
304
306
 
@@ -332,7 +334,12 @@ def ensure_resource_availability(response: GenericResponse, original: Case) -> b
332
334
  return None
333
335
 
334
336
 
335
- def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
337
+ class AuthKind(enum.Enum):
338
+ EXPLICIT = "explicit"
339
+ GENERATED = "generated"
340
+
341
+
342
+ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
336
343
  """Check if an operation declares authentication as a requirement but does not actually enforce it."""
337
344
  from .schemas import BaseOpenAPISchema
338
345
 
@@ -340,32 +347,49 @@ def ignored_auth(response: GenericResponse, case: Case) -> bool | None:
340
347
  return True
341
348
  security_parameters = _get_security_parameters(case.operation)
342
349
  # Authentication is required for this API operation and response is successful
343
- # Will it still be successful if there is no auth?
344
350
  if security_parameters and 200 <= response.status_code < 300:
345
- if _contains_auth(response.request, security_parameters):
346
- # If there is auth in the request, then drop it and retry the call
351
+ auth = _contains_auth(ctx, response.request, security_parameters)
352
+ if auth == AuthKind.EXPLICIT:
353
+ # Auth is explicitly set, it is expected to be valid
354
+ # Check if invalid auth will give an error
347
355
  _remove_auth_from_case(case, security_parameters)
348
356
  new_response = case.operation.schema.transport.send(case)
349
357
  if 200 <= new_response.status_code < 300:
350
- # Mutate the response object in place on the best effort basis
351
- if hasattr(response, "__attrs__"):
352
- for attribute in new_response.__attrs__:
353
- setattr(response, attribute, getattr(new_response, attribute))
354
- else:
355
- response.__dict__.update(new_response.__dict__)
356
- _raise_auth_error(new_response, case.operation.verbose_name)
358
+ _update_response(response, new_response)
359
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "that requires authentication")
360
+ # Try to set invalid auth and check if it succeeds
361
+ for parameter in security_parameters:
362
+ _set_auth_for_case(case, parameter)
363
+ new_response = case.operation.schema.transport.send(case)
364
+ if 200 <= new_response.status_code < 300:
365
+ _update_response(response, new_response)
366
+ _raise_no_auth_error(new_response, case.operation.verbose_name, "with any auth")
367
+ _remove_auth_from_case(case, security_parameters)
368
+ elif auth == AuthKind.GENERATED:
369
+ # If this auth is generated which means it is likely invalid, then
370
+ # this request should have been an error
371
+ _raise_no_auth_error(response, case.operation.verbose_name, "with invalid auth")
357
372
  else:
358
373
  # Successful response when there is no auth
359
- _raise_auth_error(response, case.operation.verbose_name)
374
+ _raise_no_auth_error(response, case.operation.verbose_name, "that requires authentication")
360
375
  return None
361
376
 
362
377
 
363
- def _raise_auth_error(response: GenericResponse, operation: str) -> NoReturn:
378
+ def _update_response(old: GenericResponse, new: GenericResponse) -> None:
379
+ # Mutate the response object in place on the best effort basis
380
+ if hasattr(old, "__attrs__"):
381
+ for attribute in new.__attrs__:
382
+ setattr(old, attribute, getattr(new, attribute))
383
+ else:
384
+ old.__dict__.update(new.__dict__)
385
+
386
+
387
+ def _raise_no_auth_error(response: GenericResponse, operation: str, suffix: str) -> NoReturn:
364
388
  from ...transports.responses import get_reason
365
389
 
366
390
  exc_class = get_ignored_auth_error(operation)
367
391
  reason = get_reason(response.status_code)
368
- message = f"The API returned `{response.status_code} {reason}` for `{operation}` that requires authentication."
392
+ message = f"The API returned `{response.status_code} {reason}` for `{operation}` {suffix}."
369
393
  raise exc_class(
370
394
  failures.IgnoredAuth.title,
371
395
  context=failures.IgnoredAuth(message=message),
@@ -387,7 +411,9 @@ def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]
387
411
  ]
388
412
 
389
413
 
390
- def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityParameter]) -> bool:
414
+ def _contains_auth(
415
+ ctx: CheckContext, request: PreparedRequest, security_parameters: list[SecurityParameter]
416
+ ) -> AuthKind | None:
391
417
  """Whether a request has authentication declared in the schema."""
392
418
  from requests.cookies import RequestsCookieJar
393
419
 
@@ -410,10 +436,20 @@ def _contains_auth(request: PreparedRequest, security_parameters: list[SecurityP
410
436
  return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
411
437
 
412
438
  for parameter in security_parameters:
413
- if has_header(parameter) or has_query(parameter) or has_cookie(parameter):
414
- return True
439
+ if has_header(parameter):
440
+ if ctx.headers is not None and parameter["name"] in ctx.headers:
441
+ return AuthKind.EXPLICIT
442
+ return AuthKind.GENERATED
443
+ if has_cookie(parameter):
444
+ if ctx.headers is not None and "Cookie" in ctx.headers:
445
+ cookies = cast(RequestsCookieJar, ctx.headers["Cookie"]) # type: ignore
446
+ if parameter["name"] in cookies:
447
+ return AuthKind.EXPLICIT
448
+ return AuthKind.GENERATED
449
+ if has_query(parameter):
450
+ return AuthKind.GENERATED
415
451
 
416
- return False
452
+ return None
417
453
 
418
454
 
419
455
  def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParameter]) -> None:
@@ -431,6 +467,19 @@ def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParamet
431
467
  case.cookies.pop(name, None)
432
468
 
433
469
 
470
+ def _set_auth_for_case(case: Case, parameter: SecurityParameter) -> None:
471
+ name = parameter["name"]
472
+ for location, attr_name in (
473
+ ("header", "headers"),
474
+ ("query", "query"),
475
+ ("cookie", "cookies"),
476
+ ):
477
+ if parameter["in"] == location:
478
+ container = getattr(case, attr_name, {})
479
+ container[name] = "SCHEMATHESIS-INVALID-VALUE"
480
+ setattr(case, attr_name, container)
481
+
482
+
434
483
  @dataclass
435
484
  class ResourcePath:
436
485
  """A path to a resource with variables."""
@@ -1907,11 +1907,7 @@ _VALIDATORS = [
1907
1907
  "OPENAPI_31_VALIDATOR",
1908
1908
  ]
1909
1909
 
1910
- __all__ = [
1911
- "SWAGGER_20",
1912
- "OPENAPI_30",
1913
- "OPENAPI_31",
1914
- ] + _VALIDATORS
1910
+ __all__ = ["SWAGGER_20", "OPENAPI_30", "OPENAPI_31", *_VALIDATORS]
1915
1911
 
1916
1912
  _imports = {
1917
1913
  "SWAGGER_20_VALIDATOR": lambda: make_validator(SWAGGER_20),
@@ -4,10 +4,9 @@ from contextlib import suppress
4
4
  from dataclasses import dataclass
5
5
  from functools import lru_cache
6
6
  from itertools import chain, cycle, islice
7
- from typing import TYPE_CHECKING, Any, Generator, Union, cast
7
+ from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
8
8
 
9
9
  import requests
10
- from hypothesis.strategies import SearchStrategy
11
10
  from hypothesis_jsonschema import from_schema
12
11
 
13
12
  from ...constants import DEFAULT_RESPONSE_TIMEOUT
@@ -20,6 +19,8 @@ from .formats import STRING_FORMATS
20
19
  from .parameters import OpenAPIBody, OpenAPIParameter
21
20
 
22
21
  if TYPE_CHECKING:
22
+ from hypothesis.strategies import SearchStrategy
23
+
23
24
  from ...generation import GenerationConfig
24
25
 
25
26
 
@@ -77,6 +78,7 @@ def get_strategies_from_examples(
77
78
 
78
79
  def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Generator[Example, None, None]:
79
80
  """Extract top-level parameter examples from `examples` & `example` fields."""
81
+ responses = find_in_responses(operation)
80
82
  for parameter in operation.iter_parameters():
81
83
  if "schema" in parameter.definition:
82
84
  definitions = [parameter.definition, *_expand_subschemas(parameter.definition["schema"])]
@@ -106,6 +108,10 @@ def extract_top_level(operation: APIOperation[OpenAPIParameter, Case]) -> Genera
106
108
  yield ParameterExample(
107
109
  container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
108
110
  )
111
+ for value in find_matching_in_responses(responses, parameter.name):
112
+ yield ParameterExample(
113
+ container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
114
+ )
109
115
  for alternative in operation.body:
110
116
  alternative = cast(OpenAPIBody, alternative)
111
117
  if "schema" in alternative.definition:
@@ -349,3 +355,87 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
349
355
  }
350
356
  for container, variants in parameters.items()
351
357
  }
358
+
359
+
360
+ def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
361
+ """Find schema examples in responses."""
362
+ output: dict[str, list[dict[str, Any]]] = {}
363
+ for status_code, response in operation.definition.raw.get("responses", {}).items():
364
+ if not str(status_code).startswith("2"):
365
+ # Check only 2xx responses
366
+ continue
367
+ if isinstance(response, dict) and "$ref" in response:
368
+ _, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
369
+ for media_type, definition in response.get("content", {}).items():
370
+ schema_ref = definition.get("schema", {}).get("$ref")
371
+ if schema_ref:
372
+ name = schema_ref.split("/")[-1]
373
+ else:
374
+ name = f"{status_code}/{media_type}"
375
+ for examples_field, example_field in (
376
+ ("examples", "example"),
377
+ ("x-examples", "x-example"),
378
+ ):
379
+ examples = definition.get(examples_field, {})
380
+ for example in examples.values():
381
+ if "value" in example:
382
+ output.setdefault(name, []).append(example["value"])
383
+ if example_field in definition:
384
+ output.setdefault(name, []).append(definition[example_field])
385
+ return output
386
+
387
+
388
+ NOT_FOUND = object()
389
+
390
+
391
+ def find_matching_in_responses(examples: dict[str, list], param: str) -> Iterator[Any]:
392
+ """Find matching parameter examples."""
393
+ normalized = param.lower()
394
+ is_id_param = normalized.endswith("id")
395
+ # Extract values from response examples that match input parameters.
396
+ # E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
397
+ # as examples for the "id" path parameter.
398
+ for schema_name, schema_examples in examples.items():
399
+ for example in schema_examples:
400
+ if not isinstance(example, dict):
401
+ continue
402
+ # Unwrapping example from `{"item": [{...}]}`
403
+ if isinstance(example, dict) and len(example) == 1 and list(example)[0].lower() == schema_name.lower():
404
+ inner = list(example.values())[0]
405
+ if isinstance(inner, list):
406
+ for sub_example in inner:
407
+ found = _find_matching_in_responses(sub_example, schema_name, param, normalized, is_id_param)
408
+ if found is not NOT_FOUND:
409
+ yield found
410
+ continue
411
+ example = inner
412
+ found = _find_matching_in_responses(example, schema_name, param, normalized, is_id_param)
413
+ if found is not NOT_FOUND:
414
+ yield found
415
+
416
+
417
+ def _find_matching_in_responses(
418
+ example: dict[str, Any], schema_name: str, param: str, normalized: str, is_id_param: bool
419
+ ) -> Any:
420
+ # Check for exact match
421
+ if param in example:
422
+ return example[param]
423
+
424
+ # Check for case-insensitive match
425
+ for key in example:
426
+ if key.lower() == normalized:
427
+ return example[key]
428
+ else:
429
+ # If no match found and it's an ID parameter, try additional checks
430
+ if is_id_param:
431
+ # Check for 'id' if parameter is '{something}Id'
432
+ if "id" in example:
433
+ return example["id"]
434
+ # Check for '{schemaName}Id' or '{schemaName}_id'
435
+ if normalized == "id" or normalized.startswith(schema_name.lower()):
436
+ for key in (schema_name, schema_name.lower()):
437
+ for suffix in ("_id", "Id"):
438
+ with_suffix = f"{key}{suffix}"
439
+ if with_suffix in example:
440
+ return example[with_suffix]
441
+ return NOT_FOUND
@@ -11,6 +11,13 @@ from typing import Any
11
11
  from . import lexer, nodes, parser
12
12
  from .context import ExpressionContext
13
13
 
14
+ __all__ = [
15
+ "lexer",
16
+ "nodes",
17
+ "parser",
18
+ "ExpressionContext",
19
+ ]
20
+
14
21
 
15
22
  def evaluate(expr: Any, context: ExpressionContext, evaluate_nested: bool = False) -> Any:
16
23
  """Evaluate runtime expression in context."""
@@ -1,7 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ import re
5
8
 
6
9
 
7
10
  @dataclass
@@ -4,13 +4,15 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum, unique
7
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from requests.structures import CaseInsensitiveDict
10
10
 
11
11
  from .. import references
12
- from .context import ExpressionContext
13
- from .extractors import Extractor
12
+
13
+ if TYPE_CHECKING:
14
+ from .context import ExpressionContext
15
+ from .extractors import Extractor
14
16
 
15
17
 
16
18
  @dataclass
@@ -10,22 +10,22 @@ from difflib import get_close_matches
10
10
  from types import SimpleNamespace
11
11
  from typing import TYPE_CHECKING, Any, Generator, Literal, NoReturn, Sequence, TypedDict, Union, cast
12
12
 
13
- from jsonschema import RefResolver
14
-
15
13
  from ...constants import NOT_SET
16
14
  from ...internal.copy import fast_deepcopy
17
15
  from ...models import APIOperation, Case, TransitionId
18
- from ...parameters import ParameterSet
19
16
  from ...stateful import ParsedData, StatefulTest, UnresolvableLink
20
17
  from ...stateful.state_machine import Direction
21
- from ...types import NotSet
22
18
  from . import expressions
23
19
  from .constants import LOCATION_TO_CONTAINER
24
20
  from .parameters import OpenAPI20Body, OpenAPI30Body, OpenAPIParameter
25
21
  from .references import RECURSION_DEPTH_LIMIT, Unresolvable
26
22
 
27
23
  if TYPE_CHECKING:
24
+ from jsonschema import RefResolver
25
+
26
+ from ...parameters import ParameterSet
28
27
  from ...transports.responses import GenericResponse
28
+ from ...types import NotSet
29
29
 
30
30
 
31
31
  @dataclass(repr=False)
@@ -9,7 +9,7 @@ from urllib.parse import urljoin
9
9
 
10
10
  from ... import experimental, fixups
11
11
  from ...code_samples import CodeSampleStyle
12
- from ...constants import NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
12
+ from ...constants import DEFAULT_RESPONSE_TIMEOUT, NOT_SET, WAIT_FOR_SCHEMA_INTERVAL
13
13
  from ...exceptions import SchemaError, SchemaErrorType
14
14
  from ...filters import filter_set_from_components
15
15
  from ...generation import (
@@ -163,11 +163,12 @@ def from_uri(
163
163
  interval=WAIT_FOR_SCHEMA_INTERVAL,
164
164
  )
165
165
  def _load_schema(_uri: str, **_kwargs: Any) -> requests.Response:
166
- return requests.get(_uri, **kwargs)
166
+ return requests.get(_uri, **_kwargs)
167
167
 
168
168
  else:
169
169
  _load_schema = requests.get
170
170
 
171
+ kwargs.setdefault("timeout", DEFAULT_RESPONSE_TIMEOUT / 1000)
171
172
  response = load_schema_from_url(lambda: _load_schema(uri, **kwargs))
172
173
  return from_file(
173
174
  response.text,
@@ -441,7 +442,7 @@ def _format_status_codes(status_codes: list[tuple[int, list[str | int]]]) -> str
441
442
  for status_code, path in status_codes:
442
443
  buffer.write(f" - {status_code} at schema['paths']")
443
444
  for chunk in path:
444
- buffer.write(f"[{repr(chunk)}]")
445
+ buffer.write(f"[{chunk!r}]")
445
446
  buffer.write("['responses']\n")
446
447
  return buffer.getvalue().rstrip()
447
448
 
@@ -595,9 +596,9 @@ def from_wsgi(
595
596
 
596
597
 
597
598
  def get_loader_for_app(app: Any) -> Callable:
598
- from starlette.applications import Starlette
599
+ from ...transports.asgi import is_asgi_app
599
600
 
600
- if isinstance(app, Starlette):
601
+ if is_asgi_app(app):
601
602
  return from_asgi
602
603
  if app.__class__.__module__.startswith("aiohttp."):
603
604
  return from_aiohttp
@@ -2,17 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from functools import lru_cache
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
  from urllib.parse import urlencode
7
7
 
8
8
  import jsonschema
9
9
  from hypothesis import strategies as st
10
10
  from hypothesis_jsonschema import from_schema
11
11
 
12
- from ....generation import GenerationConfig
13
12
  from ..constants import ALL_KEYWORDS
14
13
  from .mutations import MutationContext
15
- from .types import Draw, Schema
14
+
15
+ if TYPE_CHECKING:
16
+ from ....generation import GenerationConfig
17
+ from .types import Draw, Schema
16
18
 
17
19
 
18
20
  @dataclass
@@ -179,7 +179,7 @@ def remove_required_property(context: MutationContext, draw: Draw, schema: Schem
179
179
  else:
180
180
  candidate = draw(st.sampled_from(sorted(required)))
181
181
  enabled_properties = draw(st.shared(FeatureStrategy(), key="properties")) # type: ignore
182
- candidates = [candidate] + sorted([prop for prop in required if enabled_properties.is_enabled(prop)])
182
+ candidates = [candidate, *sorted([prop for prop in required if enabled_properties.is_enabled(prop)])]
183
183
  property_name = draw(st.sampled_from(candidates))
184
184
  required.remove(property_name)
185
185
  if not required:
@@ -226,9 +226,10 @@ def change_type(context: MutationContext, draw: Draw, schema: Schema) -> Mutatio
226
226
  candidate = draw(st.sampled_from(sorted(candidates)))
227
227
  candidates.remove(candidate)
228
228
  enabled_types = draw(st.shared(FeatureStrategy(), key="types")) # type: ignore
229
- remaining_candidates = [candidate] + sorted(
230
- [candidate for candidate in candidates if enabled_types.is_enabled(candidate)]
231
- )
229
+ remaining_candidates = [
230
+ candidate,
231
+ *sorted([candidate for candidate in candidates if enabled_types.is_enabled(candidate)]),
232
+ ]
232
233
  new_type = draw(st.sampled_from(remaining_candidates))
233
234
  schema["type"] = new_type
234
235
  prevent_unsatisfiable_schema(schema, new_type)
@@ -2,13 +2,15 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from dataclasses import dataclass
5
- from typing import Any, ClassVar, Iterable
5
+ from typing import TYPE_CHECKING, Any, ClassVar, Iterable
6
6
 
7
7
  from ...exceptions import OperationSchemaError
8
- from ...models import APIOperation
9
8
  from ...parameters import Parameter
10
9
  from .converter import to_json_schema_recursive
11
10
 
11
+ if TYPE_CHECKING:
12
+ from ...models import APIOperation
13
+
12
14
 
13
15
  @dataclass(eq=False)
14
16
  class OpenAPIParameter(Parameter):