schemathesis 4.1.4__py3-none-any.whl → 4.2.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.
- schemathesis/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +87 -11
- schemathesis/generation/hypothesis/__init__.py +4 -1
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,16 +4,17 @@ import enum
|
|
4
4
|
import http.client
|
5
5
|
from dataclasses import dataclass
|
6
6
|
from http.cookies import SimpleCookie
|
7
|
-
from typing import TYPE_CHECKING, Any,
|
7
|
+
from typing import TYPE_CHECKING, Any, Iterator, Mapping, NoReturn, cast
|
8
8
|
from urllib.parse import parse_qs, urlparse
|
9
9
|
|
10
10
|
import schemathesis
|
11
11
|
from schemathesis.checks import CheckContext
|
12
12
|
from schemathesis.core import media_types, string_to_boolean
|
13
13
|
from schemathesis.core.failures import Failure
|
14
|
+
from schemathesis.core.parameters import ParameterLocation
|
14
15
|
from schemathesis.core.transport import Response
|
15
16
|
from schemathesis.generation.case import Case
|
16
|
-
from schemathesis.generation.meta import
|
17
|
+
from schemathesis.generation.meta import CoveragePhaseData
|
17
18
|
from schemathesis.openapi.checks import (
|
18
19
|
AcceptedNegativeData,
|
19
20
|
EnsureResourceAvailability,
|
@@ -29,13 +30,13 @@ from schemathesis.openapi.checks import (
|
|
29
30
|
UnsupportedMethodResponse,
|
30
31
|
UseAfterFree,
|
31
32
|
)
|
32
|
-
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
33
33
|
from schemathesis.transport.prepare import prepare_path
|
34
34
|
|
35
35
|
from .utils import expand_status_code, expand_status_codes
|
36
36
|
|
37
37
|
if TYPE_CHECKING:
|
38
38
|
from schemathesis.schemas import APIOperation
|
39
|
+
from schemathesis.specs.openapi.adapter.parameters import OpenApiParameterSet
|
39
40
|
|
40
41
|
|
41
42
|
def is_unexpected_http_status_case(case: Case) -> bool:
|
@@ -53,13 +54,13 @@ def status_code_conformance(ctx: CheckContext, response: Response, case: Case) -
|
|
53
54
|
|
54
55
|
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
55
56
|
return True
|
56
|
-
|
57
|
+
status_codes = case.operation.responses.status_codes
|
57
58
|
# "default" can be used as the default response object for all HTTP codes that are not covered individually
|
58
|
-
if "default" in
|
59
|
+
if "default" in status_codes:
|
59
60
|
return None
|
60
|
-
allowed_status_codes = list(
|
61
|
+
allowed_status_codes = list(_expand_status_codes(status_codes))
|
61
62
|
if response.status_code not in allowed_status_codes:
|
62
|
-
defined_status_codes = list(map(str,
|
63
|
+
defined_status_codes = list(map(str, status_codes))
|
63
64
|
responses_list = ", ".join(defined_status_codes)
|
64
65
|
raise UndefinedStatusCode(
|
65
66
|
operation=case.operation.label,
|
@@ -71,7 +72,7 @@ def status_code_conformance(ctx: CheckContext, response: Response, case: Case) -
|
|
71
72
|
return None # explicitly return None for mypy
|
72
73
|
|
73
74
|
|
74
|
-
def
|
75
|
+
def _expand_status_codes(responses: tuple[str, ...]) -> Iterator[int]:
|
75
76
|
for code in responses:
|
76
77
|
yield from expand_status_code(code)
|
77
78
|
|
@@ -131,60 +132,48 @@ def _reraise_malformed_media_type(case: Case, location: str, actual: str, define
|
|
131
132
|
def response_headers_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
132
133
|
import jsonschema
|
133
134
|
|
134
|
-
from .
|
135
|
-
from .schemas import BaseOpenAPISchema, OpenApi30, _maybe_raise_one_or_more
|
135
|
+
from .schemas import BaseOpenAPISchema, _maybe_raise_one_or_more
|
136
136
|
|
137
137
|
if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
|
138
138
|
return True
|
139
|
-
|
140
|
-
|
139
|
+
|
140
|
+
# Find the matching response definition
|
141
|
+
response_definition = case.operation.responses.find_by_status_code(response.status_code)
|
142
|
+
if response_definition is None:
|
141
143
|
return None
|
142
|
-
|
143
|
-
|
144
|
+
# Check whether the matching response definition has headers defined
|
145
|
+
headers = response_definition.headers
|
146
|
+
if not headers:
|
144
147
|
return None
|
145
148
|
|
146
|
-
missing_headers = [
|
147
|
-
header
|
148
|
-
for header, definition in defined_headers.items()
|
149
|
-
if header.lower() not in response.headers and definition.get(case.operation.schema.header_required_field, False)
|
150
|
-
]
|
151
149
|
errors: list[Failure] = []
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
for name, definition in defined_headers.items():
|
150
|
+
|
151
|
+
missing_headers = []
|
152
|
+
|
153
|
+
for name, header in headers.items():
|
157
154
|
values = response.headers.get(name.lower())
|
158
155
|
if values is not None:
|
159
156
|
value = values[0]
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
coerced = _coerce_header_value(value, schema)
|
171
|
-
try:
|
172
|
-
jsonschema.validate(
|
173
|
-
coerced,
|
174
|
-
schema,
|
175
|
-
cls=case.operation.schema.validator_cls,
|
176
|
-
resolver=resolver,
|
177
|
-
format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
|
178
|
-
)
|
179
|
-
except jsonschema.ValidationError as exc:
|
180
|
-
errors.append(
|
181
|
-
JsonSchemaError.from_exception(
|
182
|
-
title="Response header does not conform to the schema",
|
183
|
-
operation=case.operation.label,
|
184
|
-
exc=exc,
|
185
|
-
config=case.operation.schema.config.output,
|
186
|
-
)
|
157
|
+
coerced = _coerce_header_value(value, header.schema)
|
158
|
+
try:
|
159
|
+
header.validator.validate(coerced)
|
160
|
+
except jsonschema.ValidationError as exc:
|
161
|
+
errors.append(
|
162
|
+
JsonSchemaError.from_exception(
|
163
|
+
title="Response header does not conform to the schema",
|
164
|
+
operation=case.operation.label,
|
165
|
+
exc=exc,
|
166
|
+
config=case.operation.schema.config.output,
|
187
167
|
)
|
168
|
+
)
|
169
|
+
elif header.is_required:
|
170
|
+
missing_headers.append(name)
|
171
|
+
|
172
|
+
if missing_headers:
|
173
|
+
formatted_headers = [f"\n- `{header}`" for header in missing_headers]
|
174
|
+
message = f"The following required headers are missing from the response:{''.join(formatted_headers)}"
|
175
|
+
errors.append(MissingHeaders(operation=case.operation.label, message=message, missing_headers=missing_headers))
|
176
|
+
|
188
177
|
return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
|
189
178
|
|
190
179
|
|
@@ -279,7 +268,7 @@ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -
|
|
279
268
|
data = meta.phase.data
|
280
269
|
if (
|
281
270
|
data.parameter
|
282
|
-
and data.parameter_location ==
|
271
|
+
and data.parameter_location == ParameterLocation.HEADER
|
283
272
|
and data.description
|
284
273
|
and data.description.startswith("Missing ")
|
285
274
|
):
|
@@ -334,25 +323,30 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
334
323
|
# This function is used to determine if negation is solely in the form of extra properties,
|
335
324
|
# which are often ignored for backward-compatibility by the tested apps
|
336
325
|
from ._hypothesis import get_schema_for_location
|
326
|
+
from .schemas import BaseOpenAPISchema
|
337
327
|
|
338
328
|
meta = case.meta
|
339
|
-
if meta is None:
|
329
|
+
if meta is None or not isinstance(case.operation.schema, BaseOpenAPISchema):
|
340
330
|
# Ignore manually created cases
|
341
331
|
return False
|
342
|
-
if (
|
343
|
-
|
344
|
-
and meta.components[ComponentKind.PATH_PARAMETERS].mode.is_negative
|
332
|
+
if (ParameterLocation.BODY in meta.components and meta.components[ParameterLocation.BODY].mode.is_negative) or (
|
333
|
+
ParameterLocation.PATH in meta.components and meta.components[ParameterLocation.PATH].mode.is_negative
|
345
334
|
):
|
346
335
|
# Body or path negations always imply other negations
|
347
336
|
return False
|
348
|
-
validator_cls = case.operation.schema.
|
349
|
-
for
|
350
|
-
meta_for_location = meta.components.get(
|
351
|
-
value = getattr(case,
|
337
|
+
validator_cls = case.operation.schema.adapter.jsonschema_validator_cls
|
338
|
+
for location in (ParameterLocation.QUERY, ParameterLocation.HEADER, ParameterLocation.COOKIE):
|
339
|
+
meta_for_location = meta.components.get(location)
|
340
|
+
value = getattr(case, location.container_name)
|
352
341
|
if value is not None and meta_for_location is not None and meta_for_location.mode.is_negative:
|
353
|
-
|
354
|
-
|
355
|
-
|
342
|
+
container = getattr(case.operation, location.container_name)
|
343
|
+
schema = get_schema_for_location(location, container)
|
344
|
+
|
345
|
+
if _has_serialization_sensitive_types(schema, container):
|
346
|
+
# Can't reliably determine if only additional properties were added
|
347
|
+
continue
|
348
|
+
|
349
|
+
value_without_additional_properties = {k: v for k, v in value.items() if k in container}
|
356
350
|
if not validator_cls(schema).is_valid(value_without_additional_properties):
|
357
351
|
# Other types of negation found
|
358
352
|
return False
|
@@ -360,6 +354,30 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
|
|
360
354
|
return True
|
361
355
|
|
362
356
|
|
357
|
+
def _has_serialization_sensitive_types(schema: dict, container: OpenApiParameterSet) -> bool:
|
358
|
+
"""Check if schema contains array or object types in defined parameters.
|
359
|
+
|
360
|
+
In query/header/cookie parameters, arrays and objects are serialized to strings.
|
361
|
+
This makes post-serialization validation against the original schema unreliable:
|
362
|
+
|
363
|
+
- Generated: ["foo", "bar"] (array)
|
364
|
+
- Serialized: "foo,bar" (string)
|
365
|
+
|
366
|
+
Validation of string against array schema fails incorrectly.
|
367
|
+
A better approach would be to apply serialization later on in the process.
|
368
|
+
|
369
|
+
"""
|
370
|
+
from schemathesis.core.jsonschema import get_type
|
371
|
+
|
372
|
+
properties = schema.get("properties", {})
|
373
|
+
for prop_name, prop_schema in properties.items():
|
374
|
+
if prop_name in container:
|
375
|
+
types = get_type(prop_schema)
|
376
|
+
if "array" in types or "object" in types:
|
377
|
+
return True
|
378
|
+
return False
|
379
|
+
|
380
|
+
|
363
381
|
@schemathesis.check
|
364
382
|
def use_after_free(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
365
383
|
from .schemas import BaseOpenAPISchema
|
@@ -439,7 +457,7 @@ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Ca
|
|
439
457
|
overrides = case._override
|
440
458
|
overrides_all_parameters = True
|
441
459
|
for parameter in case.operation.iter_parameters():
|
442
|
-
container =
|
460
|
+
container = parameter.location.container_name
|
443
461
|
if parameter.name not in getattr(overrides, container, {}):
|
444
462
|
overrides_all_parameters = False
|
445
463
|
break
|
@@ -495,12 +513,14 @@ class AuthKind(str, enum.Enum):
|
|
495
513
|
@schemathesis.check
|
496
514
|
def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | None:
|
497
515
|
"""Check if an operation declares authentication as a requirement but does not actually enforce it."""
|
498
|
-
from .
|
516
|
+
from schemathesis.specs.openapi.adapter.security import has_optional_auth
|
517
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
499
518
|
|
519
|
+
operation = case.operation
|
500
520
|
if (
|
501
|
-
not isinstance(
|
521
|
+
not isinstance(operation.schema, BaseOpenAPISchema)
|
502
522
|
or is_unexpected_http_status_case(case)
|
503
|
-
or
|
523
|
+
or has_optional_auth(operation.schema.raw_schema, operation.definition.raw)
|
504
524
|
):
|
505
525
|
return True
|
506
526
|
security_parameters = _get_security_parameters(case.operation)
|
@@ -574,30 +594,19 @@ def _raise_no_auth_error(response: Response, case: Case, auth: AuthScenario) ->
|
|
574
594
|
)
|
575
595
|
|
576
596
|
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
def _get_security_parameters(operation: APIOperation) -> list[SecurityParameter]:
|
597
|
+
def _get_security_parameters(operation: APIOperation) -> list[Mapping[str, Any]]:
|
581
598
|
"""Extract security definitions that are active for the given operation and convert them into parameters."""
|
582
|
-
from .
|
599
|
+
from schemathesis.specs.openapi.adapter.security import ORIGINAL_SECURITY_TYPE_KEY
|
583
600
|
|
584
|
-
schema = cast(BaseOpenAPISchema, operation.schema)
|
585
601
|
return [
|
586
|
-
|
587
|
-
for
|
588
|
-
if
|
602
|
+
param
|
603
|
+
for param in operation.security.iter_parameters()
|
604
|
+
if param[ORIGINAL_SECURITY_TYPE_KEY] in ["apiKey", "basic", "http"]
|
589
605
|
]
|
590
606
|
|
591
607
|
|
592
|
-
def _has_optional_auth(operation: APIOperation) -> bool:
|
593
|
-
from .schemas import BaseOpenAPISchema
|
594
|
-
|
595
|
-
schema = cast(BaseOpenAPISchema, operation.schema)
|
596
|
-
return schema.security.has_optional_auth(schema.raw_schema, operation)
|
597
|
-
|
598
|
-
|
599
608
|
def _contains_auth(
|
600
|
-
ctx: CheckContext, case: Case, response: Response, security_parameters: list[
|
609
|
+
ctx: CheckContext, case: Case, response: Response, security_parameters: list[Mapping[str, Any]]
|
601
610
|
) -> AuthKind | None:
|
602
611
|
"""Whether a request has authentication declared in the schema."""
|
603
612
|
from requests.cookies import RequestsCookieJar
|
@@ -614,13 +623,13 @@ def _contains_auth(
|
|
614
623
|
if raw_cookie is not None:
|
615
624
|
header_cookies.load(raw_cookie)
|
616
625
|
|
617
|
-
def has_header(p:
|
626
|
+
def has_header(p: Mapping[str, Any]) -> bool:
|
618
627
|
return p["in"] == "header" and p["name"] in request.headers
|
619
628
|
|
620
|
-
def has_query(p:
|
629
|
+
def has_query(p: Mapping[str, Any]) -> bool:
|
621
630
|
return p["in"] == "query" and p["name"] in query
|
622
631
|
|
623
|
-
def has_cookie(p:
|
632
|
+
def has_cookie(p: Mapping[str, Any]) -> bool:
|
624
633
|
cookies = cast(RequestsCookieJar, request._cookies) # type: ignore
|
625
634
|
return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
|
626
635
|
|
@@ -662,7 +671,7 @@ def _contains_auth(
|
|
662
671
|
return None
|
663
672
|
|
664
673
|
|
665
|
-
def remove_auth(case: Case, security_parameters: list[
|
674
|
+
def remove_auth(case: Case, security_parameters: list[Mapping[str, Any]]) -> Case:
|
666
675
|
"""Remove security parameters from a generated case.
|
667
676
|
|
668
677
|
It mutates `case` in place.
|
@@ -692,14 +701,14 @@ def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Cas
|
|
692
701
|
)
|
693
702
|
|
694
703
|
|
695
|
-
def _remove_auth_from_container(container: dict, security_parameters: list[
|
704
|
+
def _remove_auth_from_container(container: dict, security_parameters: list[Mapping[str, Any]], location: str) -> None:
|
696
705
|
for parameter in security_parameters:
|
697
706
|
name = parameter["name"]
|
698
707
|
if parameter["in"] == location:
|
699
708
|
container.pop(name, None)
|
700
709
|
|
701
710
|
|
702
|
-
def _set_auth_for_case(case: Case, parameter:
|
711
|
+
def _set_auth_for_case(case: Case, parameter: Mapping[str, Any]) -> None:
|
703
712
|
name = parameter["name"]
|
704
713
|
for location, attr_name in (
|
705
714
|
("header", "headers"),
|
@@ -1,31 +1,69 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from itertools import chain
|
4
|
-
from typing import Any, Callable
|
4
|
+
from typing import Any, Callable, overload
|
5
5
|
|
6
|
-
from schemathesis.core.
|
7
|
-
|
8
|
-
from .
|
6
|
+
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
7
|
+
from schemathesis.core.jsonschema.types import JsonSchema
|
8
|
+
from schemathesis.core.transforms import deepclone
|
9
|
+
from schemathesis.specs.openapi.patterns import update_quantifier
|
9
10
|
|
10
11
|
|
12
|
+
@overload
|
11
13
|
def to_json_schema(
|
12
14
|
schema: dict[str, Any],
|
13
|
-
|
14
|
-
nullable_name: str,
|
15
|
-
copy: bool = True,
|
15
|
+
nullable_keyword: str,
|
16
16
|
is_response_schema: bool = False,
|
17
17
|
update_quantifiers: bool = True,
|
18
|
-
|
19
|
-
|
18
|
+
clone: bool = True,
|
19
|
+
) -> dict[str, Any]: ... # pragma: no cover
|
20
|
+
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
@overload
|
23
|
+
def to_json_schema(
|
24
|
+
schema: bool,
|
25
|
+
nullable_keyword: str,
|
26
|
+
is_response_schema: bool = False,
|
27
|
+
update_quantifiers: bool = True,
|
28
|
+
clone: bool = True,
|
29
|
+
) -> bool: ... # pragma: no cover
|
30
|
+
|
31
|
+
|
32
|
+
def to_json_schema(
|
33
|
+
schema: dict[str, Any] | bool,
|
34
|
+
nullable_keyword: str,
|
35
|
+
is_response_schema: bool = False,
|
36
|
+
update_quantifiers: bool = True,
|
37
|
+
clone: bool = True,
|
38
|
+
) -> dict[str, Any] | bool:
|
39
|
+
if isinstance(schema, bool):
|
40
|
+
return schema
|
41
|
+
if clone:
|
25
42
|
schema = deepclone(schema)
|
26
|
-
|
27
|
-
|
43
|
+
return _to_json_schema(
|
44
|
+
schema,
|
45
|
+
nullable_keyword=nullable_keyword,
|
46
|
+
is_response_schema=is_response_schema,
|
47
|
+
update_quantifiers=update_quantifiers,
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
def _to_json_schema(
|
52
|
+
schema: JsonSchema,
|
53
|
+
*,
|
54
|
+
nullable_keyword: str,
|
55
|
+
is_response_schema: bool = False,
|
56
|
+
update_quantifiers: bool = True,
|
57
|
+
) -> JsonSchema:
|
58
|
+
if isinstance(schema, bool):
|
59
|
+
return schema
|
60
|
+
|
61
|
+
if schema.get(nullable_keyword) is True:
|
62
|
+
del schema[nullable_keyword]
|
63
|
+
bundled = schema.pop(BUNDLE_STORAGE_KEY, None)
|
28
64
|
schema = {"anyOf": [schema, {"type": "null"}]}
|
65
|
+
if bundled:
|
66
|
+
schema[BUNDLE_STORAGE_KEY] = bundled
|
29
67
|
schema_type = schema.get("type")
|
30
68
|
if schema_type == "file":
|
31
69
|
schema["type"] = "string"
|
@@ -54,9 +92,70 @@ def to_json_schema(
|
|
54
92
|
else:
|
55
93
|
# Read-only properties should not occur in requests
|
56
94
|
rewrite_properties(schema, is_read_only)
|
95
|
+
|
96
|
+
for keyword, value in schema.items():
|
97
|
+
if keyword in IN_VALUE and isinstance(value, dict):
|
98
|
+
schema[keyword] = _to_json_schema(
|
99
|
+
value,
|
100
|
+
nullable_keyword=nullable_keyword,
|
101
|
+
is_response_schema=is_response_schema,
|
102
|
+
update_quantifiers=update_quantifiers,
|
103
|
+
)
|
104
|
+
elif keyword in IN_ITEM and isinstance(value, list):
|
105
|
+
for idx, subschema in enumerate(value):
|
106
|
+
value[idx] = _to_json_schema(
|
107
|
+
subschema,
|
108
|
+
nullable_keyword=nullable_keyword,
|
109
|
+
is_response_schema=is_response_schema,
|
110
|
+
update_quantifiers=update_quantifiers,
|
111
|
+
)
|
112
|
+
elif keyword in IN_CHILD and isinstance(value, dict):
|
113
|
+
for name, subschema in value.items():
|
114
|
+
value[name] = _to_json_schema(
|
115
|
+
subschema,
|
116
|
+
nullable_keyword=nullable_keyword,
|
117
|
+
is_response_schema=is_response_schema,
|
118
|
+
update_quantifiers=update_quantifiers,
|
119
|
+
)
|
120
|
+
|
57
121
|
return schema
|
58
122
|
|
59
123
|
|
124
|
+
IN_VALUE = frozenset(
|
125
|
+
(
|
126
|
+
"additionalProperties",
|
127
|
+
"contains",
|
128
|
+
"contentSchema",
|
129
|
+
"else",
|
130
|
+
"if",
|
131
|
+
"items",
|
132
|
+
"not",
|
133
|
+
"propertyNames",
|
134
|
+
"then",
|
135
|
+
"unevaluatedItems",
|
136
|
+
"unevaluatedProperties",
|
137
|
+
)
|
138
|
+
)
|
139
|
+
IN_ITEM = frozenset(
|
140
|
+
(
|
141
|
+
"allOf",
|
142
|
+
"anyOf",
|
143
|
+
"oneOf",
|
144
|
+
)
|
145
|
+
)
|
146
|
+
IN_CHILD = frozenset(
|
147
|
+
(
|
148
|
+
"prefixItems",
|
149
|
+
"$defs",
|
150
|
+
"definitions",
|
151
|
+
"dependentSchemas",
|
152
|
+
"patternProperties",
|
153
|
+
"properties",
|
154
|
+
BUNDLE_STORAGE_KEY,
|
155
|
+
)
|
156
|
+
)
|
157
|
+
|
158
|
+
|
60
159
|
def update_pattern_in_schema(schema: dict[str, Any]) -> None:
|
61
160
|
pattern = schema.get("pattern")
|
62
161
|
min_length = schema.get("minLength")
|
@@ -104,15 +203,3 @@ def is_read_only(schema: dict[str, Any] | bool) -> bool:
|
|
104
203
|
if isinstance(schema, bool):
|
105
204
|
return False
|
106
205
|
return schema.get("readOnly", False)
|
107
|
-
|
108
|
-
|
109
|
-
def to_json_schema_recursive(
|
110
|
-
schema: dict[str, Any], nullable_name: str, is_response_schema: bool = False, update_quantifiers: bool = True
|
111
|
-
) -> dict[str, Any]:
|
112
|
-
return transform(
|
113
|
-
schema,
|
114
|
-
to_json_schema,
|
115
|
-
nullable_name=nullable_name,
|
116
|
-
is_response_schema=is_response_schema,
|
117
|
-
update_quantifiers=update_quantifiers,
|
118
|
-
)
|