schemathesis 3.26.2__py3-none-any.whl → 3.27.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/__init__.py +1 -1
- schemathesis/cli/__init__.py +1 -1
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/junitxml.py +79 -10
- schemathesis/cli/output/default.py +9 -47
- schemathesis/cli/reporting.py +72 -0
- schemathesis/generation/__init__.py +14 -2
- schemathesis/models.py +51 -143
- schemathesis/runner/impl/core.py +11 -12
- schemathesis/runner/serialization.py +32 -10
- schemathesis/schemas.py +11 -7
- schemathesis/specs/graphql/loaders.py +2 -0
- schemathesis/specs/graphql/schemas.py +7 -40
- schemathesis/specs/openapi/_hypothesis.py +16 -5
- schemathesis/specs/openapi/loaders.py +3 -0
- schemathesis/stateful/state_machine.py +3 -13
- schemathesis/transports/__init__.py +307 -0
- schemathesis/transports/responses.py +2 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/METADATA +2 -3
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/RECORD +23 -21
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/WHEEL +0 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/entry_points.txt +0 -0
- {schemathesis-3.26.2.dist-info → schemathesis-3.27.1.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from . import auths, checks, experimental, contrib, fixups, graphql, hooks, runner, serializers, targets # noqa: E402
|
|
5
5
|
from ._lazy_import import lazy_import
|
|
6
|
-
from .generation import DataGenerationMethod, GenerationConfig # noqa: E402
|
|
6
|
+
from .generation import DataGenerationMethod, GenerationConfig, HeaderConfig # noqa: E402
|
|
7
7
|
from .constants import SCHEMATHESIS_VERSION # noqa: E402
|
|
8
8
|
from .models import Case # noqa: E402
|
|
9
9
|
from .specs import openapi # noqa: E402
|
schemathesis/cli/__init__.py
CHANGED
|
@@ -1769,6 +1769,6 @@ def after_init_cli_run_handlers(
|
|
|
1769
1769
|
def process_call_kwargs(context: HookContext, case: Case, kwargs: dict[str, Any]) -> None:
|
|
1770
1770
|
"""Called before every network call in CLI tests.
|
|
1771
1771
|
|
|
1772
|
-
Aims to modify the argument passed to `case.call
|
|
1772
|
+
Aims to modify the argument passed to `case.call`.
|
|
1773
1773
|
Note that you need to modify `kwargs` in-place.
|
|
1774
1774
|
"""
|
schemathesis/cli/junitxml.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
2
4
|
import platform
|
|
5
|
+
import textwrap
|
|
3
6
|
from dataclasses import dataclass, field
|
|
4
7
|
from typing import TYPE_CHECKING
|
|
5
8
|
|
|
@@ -7,12 +10,14 @@ from junit_xml import TestCase, TestSuite, to_xml_report_file
|
|
|
7
10
|
|
|
8
11
|
from ..models import Status
|
|
9
12
|
from ..runner import events
|
|
10
|
-
from ..runner.serialization import
|
|
13
|
+
from ..runner.serialization import SerializedCheck, SerializedError
|
|
14
|
+
from ..exceptions import prepare_response_payload, RuntimeErrorType
|
|
11
15
|
from .handlers import EventHandler
|
|
12
|
-
|
|
16
|
+
from .reporting import group_by_case, TEST_CASE_ID_TITLE, split_traceback, get_runtime_error_suggestion
|
|
13
17
|
|
|
14
18
|
if TYPE_CHECKING:
|
|
15
19
|
from click.utils import LazyFile
|
|
20
|
+
|
|
16
21
|
from .context import ExecutionContext
|
|
17
22
|
|
|
18
23
|
|
|
@@ -29,15 +34,79 @@ class JunitXMLHandler(EventHandler):
|
|
|
29
34
|
allow_multiple_subelements=True,
|
|
30
35
|
)
|
|
31
36
|
if event.status == Status.failure:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
for idx, (code_sample, group) in enumerate(
|
|
38
|
+
group_by_case(event.result.checks, context.code_sample_style), 1
|
|
39
|
+
):
|
|
40
|
+
checks = sorted(group, key=lambda c: c.name != "not_a_server_error")
|
|
41
|
+
test_case.add_failure_info(message=build_failure_message(idx, code_sample, checks))
|
|
42
|
+
elif event.status == Status.error:
|
|
43
|
+
test_case.add_error_info(message=build_error_message(context, event.result.errors[-1]))
|
|
44
|
+
elif event.status == Status.skip:
|
|
45
|
+
test_case.add_skipped_info(message=event.result.skip_reason)
|
|
40
46
|
self.test_cases.append(test_case)
|
|
41
47
|
if isinstance(event, events.Finished):
|
|
42
48
|
test_suites = [TestSuite("schemathesis", test_cases=self.test_cases, hostname=platform.node())]
|
|
43
49
|
to_xml_report_file(file_descriptor=self.file_handle, test_suites=test_suites, prettyprint=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def build_failure_message(idx: int, code_sample: str, checks: list[SerializedCheck]) -> str:
|
|
53
|
+
from ..transports.responses import get_reason
|
|
54
|
+
|
|
55
|
+
message = ""
|
|
56
|
+
for check_idx, check in enumerate(checks):
|
|
57
|
+
if check_idx == 0:
|
|
58
|
+
message += f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}\n"
|
|
59
|
+
message += f"\n- {check.title}\n"
|
|
60
|
+
formatted_message = check.formatted_message
|
|
61
|
+
if formatted_message:
|
|
62
|
+
message += f"\n{formatted_message}\n"
|
|
63
|
+
if check_idx + 1 == len(checks):
|
|
64
|
+
if check.response is not None:
|
|
65
|
+
status_code = check.response.status_code
|
|
66
|
+
reason = get_reason(status_code)
|
|
67
|
+
message += f"\n[{check.response.status_code}] {reason}:\n"
|
|
68
|
+
response_body = check.response.body
|
|
69
|
+
if response_body is not None:
|
|
70
|
+
if not response_body:
|
|
71
|
+
message += "\n <EMPTY>\n"
|
|
72
|
+
else:
|
|
73
|
+
encoding = check.response.encoding or "utf8"
|
|
74
|
+
try:
|
|
75
|
+
payload = base64.b64decode(response_body).decode(encoding)
|
|
76
|
+
payload = prepare_response_payload(payload)
|
|
77
|
+
payload = textwrap.indent(f"\n`{payload}`\n", prefix=" ")
|
|
78
|
+
message += payload
|
|
79
|
+
except UnicodeDecodeError:
|
|
80
|
+
message += "\n <BINARY>\n"
|
|
81
|
+
|
|
82
|
+
message += f"\nReproduce with: \n\n {code_sample}"
|
|
83
|
+
return message
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_error_message(context: ExecutionContext, error: SerializedError) -> str:
|
|
87
|
+
message = ""
|
|
88
|
+
if error.title:
|
|
89
|
+
if error.type == RuntimeErrorType.SCHEMA_GENERIC:
|
|
90
|
+
message = "Schema Error\n"
|
|
91
|
+
else:
|
|
92
|
+
message = f"{error.title}\n"
|
|
93
|
+
if error.message:
|
|
94
|
+
message += f"\n{error.message}\n"
|
|
95
|
+
elif error.message:
|
|
96
|
+
message = error.message
|
|
97
|
+
else:
|
|
98
|
+
message = error.exception
|
|
99
|
+
if error.extras:
|
|
100
|
+
extras = error.extras
|
|
101
|
+
elif context.show_trace and error.type.has_useful_traceback:
|
|
102
|
+
extras = split_traceback(error.exception_with_traceback)
|
|
103
|
+
else:
|
|
104
|
+
extras = []
|
|
105
|
+
if extras:
|
|
106
|
+
message += "\n"
|
|
107
|
+
for extra in extras:
|
|
108
|
+
message += f" {extra}\n"
|
|
109
|
+
suggestion = get_runtime_error_suggestion(error.type, bold=str)
|
|
110
|
+
if suggestion is not None:
|
|
111
|
+
message += f"\nTip: {suggestion}"
|
|
112
|
+
return message
|
|
@@ -7,14 +7,12 @@ import shutil
|
|
|
7
7
|
import textwrap
|
|
8
8
|
import time
|
|
9
9
|
from importlib import metadata
|
|
10
|
-
from itertools import groupby
|
|
11
10
|
from queue import Queue
|
|
12
11
|
from typing import Any, Generator, cast, TYPE_CHECKING
|
|
13
12
|
|
|
14
13
|
import click
|
|
15
14
|
|
|
16
15
|
from ... import service
|
|
17
|
-
from ...code_samples import CodeSampleStyle
|
|
18
16
|
from ...constants import (
|
|
19
17
|
DISCORD_LINK,
|
|
20
18
|
FALSE_VALUES,
|
|
@@ -37,10 +35,11 @@ from ...models import Status
|
|
|
37
35
|
from ...runner import events
|
|
38
36
|
from ...runner.events import InternalErrorType, SchemaErrorType
|
|
39
37
|
from ...runner.probes import ProbeOutcome
|
|
40
|
-
from ...runner.serialization import
|
|
38
|
+
from ...runner.serialization import SerializedError, SerializedTestResult
|
|
41
39
|
from ...service.models import AnalysisSuccess, UnknownExtension, ErrorState
|
|
42
40
|
from ..context import ExecutionContext, FileReportContext, ServiceReportContext
|
|
43
41
|
from ..handlers import EventHandler
|
|
42
|
+
from ..reporting import group_by_case, TEST_CASE_ID_TITLE, split_traceback, get_runtime_error_suggestion
|
|
44
43
|
|
|
45
44
|
if TYPE_CHECKING:
|
|
46
45
|
import requests
|
|
@@ -256,11 +255,11 @@ def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
|
|
|
256
255
|
if error.extras:
|
|
257
256
|
extras = error.extras
|
|
258
257
|
elif context.show_trace and error.type.has_useful_traceback:
|
|
259
|
-
extras =
|
|
258
|
+
extras = split_traceback(error.exception_with_traceback)
|
|
260
259
|
else:
|
|
261
260
|
extras = []
|
|
262
261
|
_display_extras(extras)
|
|
263
|
-
suggestion =
|
|
262
|
+
suggestion = get_runtime_error_suggestion(error.type)
|
|
264
263
|
_maybe_display_tip(suggestion)
|
|
265
264
|
return display_full_traceback_message(error)
|
|
266
265
|
|
|
@@ -279,9 +278,6 @@ def display_failures(context: ExecutionContext, event: events.Finished) -> None:
|
|
|
279
278
|
display_failures_for_single_test(context, result)
|
|
280
279
|
|
|
281
280
|
|
|
282
|
-
TEST_CASE_ID_TITLE = "Test Case ID"
|
|
283
|
-
|
|
284
|
-
|
|
285
281
|
def display_failures_for_single_test(context: ExecutionContext, result: SerializedTestResult) -> None:
|
|
286
282
|
"""Display a failure for a single method / path."""
|
|
287
283
|
from ...transports.responses import get_reason
|
|
@@ -297,18 +293,9 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
297
293
|
for check_idx, check in enumerate(checks):
|
|
298
294
|
if check_idx == 0:
|
|
299
295
|
click.secho(f"{idx}. {TEST_CASE_ID_TITLE}: {check.example.id}", bold=True)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if check.context.message:
|
|
303
|
-
message = check.context.message
|
|
304
|
-
else:
|
|
305
|
-
message = None
|
|
306
|
-
else:
|
|
307
|
-
title = f"Custom check failed: `{check.name}`"
|
|
308
|
-
message = check.message
|
|
309
|
-
click.secho(f"\n- {title}", fg="red", bold=True)
|
|
296
|
+
click.secho(f"\n- {check.title}", fg="red", bold=True)
|
|
297
|
+
message = check.formatted_message
|
|
310
298
|
if message:
|
|
311
|
-
message = textwrap.indent(message, prefix=" ")
|
|
312
299
|
click.secho(f"\n{message}", fg="red")
|
|
313
300
|
if check_idx + 1 == len(checks):
|
|
314
301
|
if check.response is not None:
|
|
@@ -316,9 +303,8 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
316
303
|
reason = get_reason(status_code)
|
|
317
304
|
response = bold(f"[{check.response.status_code}] {reason}")
|
|
318
305
|
click.echo(f"\n{response}:")
|
|
319
|
-
|
|
320
306
|
response_body = check.response.body
|
|
321
|
-
if
|
|
307
|
+
if response_body is not None:
|
|
322
308
|
if not response_body:
|
|
323
309
|
click.echo("\n <EMPTY>")
|
|
324
310
|
else:
|
|
@@ -336,26 +322,6 @@ def display_failures_for_single_test(context: ExecutionContext, result: Serializ
|
|
|
336
322
|
)
|
|
337
323
|
|
|
338
324
|
|
|
339
|
-
def group_by_case(
|
|
340
|
-
checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
|
|
341
|
-
) -> Generator[tuple[str, Generator[SerializedCheck, None, None]], None, None]:
|
|
342
|
-
checks = deduplicate_failures(checks)
|
|
343
|
-
checks = sorted(checks, key=lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
344
|
-
yield from groupby(checks, lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def _by_unique_code_sample(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> str:
|
|
348
|
-
request_body = base64.b64decode(check.example.body).decode() if check.example.body is not None else None
|
|
349
|
-
return code_sample_style.generate(
|
|
350
|
-
method=check.example.method,
|
|
351
|
-
url=check.example.url,
|
|
352
|
-
body=request_body,
|
|
353
|
-
headers=check.example.headers,
|
|
354
|
-
verify=check.example.verify,
|
|
355
|
-
extra_headers=check.example.extra_headers,
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
325
|
def display_application_logs(context: ExecutionContext, event: events.Finished) -> None:
|
|
360
326
|
"""Print logs captured during the application run."""
|
|
361
327
|
if not event.has_logs:
|
|
@@ -436,7 +402,7 @@ def display_analysis(context: ExecutionContext) -> None:
|
|
|
436
402
|
title = "Network Error"
|
|
437
403
|
else:
|
|
438
404
|
traceback = format_exception(exception, True)
|
|
439
|
-
extras =
|
|
405
|
+
extras = split_traceback(traceback)
|
|
440
406
|
title = "Internal Error"
|
|
441
407
|
message = f"We apologize for the inconvenience. This appears to be an internal issue.\nPlease, consider reporting the following details to our issue tracker:\n\n {ISSUE_TRACKER_URL}"
|
|
442
408
|
suggestion = "Please update your CLI to the latest version and try again."
|
|
@@ -719,10 +685,6 @@ def should_skip_suggestion(context: ExecutionContext, event: events.InternalErro
|
|
|
719
685
|
return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
|
|
720
686
|
|
|
721
687
|
|
|
722
|
-
def _split_traceback(traceback: str) -> list[str]:
|
|
723
|
-
return [entry for entry in traceback.splitlines() if entry]
|
|
724
|
-
|
|
725
|
-
|
|
726
688
|
def _display_extras(extras: list[str]) -> None:
|
|
727
689
|
if extras:
|
|
728
690
|
click.echo()
|
|
@@ -743,7 +705,7 @@ def display_internal_error(context: ExecutionContext, event: events.InternalErro
|
|
|
743
705
|
if event.type == InternalErrorType.SCHEMA:
|
|
744
706
|
extras = event.extras
|
|
745
707
|
elif context.show_trace:
|
|
746
|
-
extras =
|
|
708
|
+
extras = split_traceback(event.exception_with_traceback)
|
|
747
709
|
else:
|
|
748
710
|
extras = [event.exception]
|
|
749
711
|
_display_extras(extras)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from itertools import groupby
|
|
5
|
+
from typing import Callable, Generator
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from ..code_samples import CodeSampleStyle
|
|
10
|
+
from ..exceptions import RuntimeErrorType
|
|
11
|
+
from ..runner.serialization import SerializedCheck, deduplicate_failures
|
|
12
|
+
|
|
13
|
+
TEST_CASE_ID_TITLE = "Test Case ID"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def group_by_case(
|
|
17
|
+
checks: list[SerializedCheck], code_sample_style: CodeSampleStyle
|
|
18
|
+
) -> Generator[tuple[str, Generator[SerializedCheck, None, None]], None, None]:
|
|
19
|
+
checks = deduplicate_failures(checks)
|
|
20
|
+
checks = sorted(checks, key=lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
21
|
+
yield from groupby(checks, lambda c: _by_unique_code_sample(c, code_sample_style))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _by_unique_code_sample(check: SerializedCheck, code_sample_style: CodeSampleStyle) -> str:
|
|
25
|
+
request_body = base64.b64decode(check.example.body).decode() if check.example.body is not None else None
|
|
26
|
+
return code_sample_style.generate(
|
|
27
|
+
method=check.example.method,
|
|
28
|
+
url=check.example.url,
|
|
29
|
+
body=request_body,
|
|
30
|
+
headers=check.example.headers,
|
|
31
|
+
verify=check.example.verify,
|
|
32
|
+
extra_headers=check.example.extra_headers,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def split_traceback(traceback: str) -> list[str]:
|
|
37
|
+
return [entry for entry in traceback.splitlines() if entry]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bold(option: str) -> str:
|
|
41
|
+
return click.style(option, bold=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_runtime_error_suggestion(error_type: RuntimeErrorType, bold: Callable[[str], str] = bold) -> str | None:
|
|
45
|
+
DISABLE_SSL_SUGGESTION = f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}."
|
|
46
|
+
DISABLE_SCHEMA_VALIDATION_SUGGESTION = (
|
|
47
|
+
f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _format_health_check_suggestion(label: str) -> str:
|
|
51
|
+
return f"Bypass this health check using {bold(f'`--hypothesis-suppress-health-check={label}`')}."
|
|
52
|
+
|
|
53
|
+
RUNTIME_ERROR_SUGGESTIONS = {
|
|
54
|
+
RuntimeErrorType.CONNECTION_SSL: DISABLE_SSL_SUGGESTION,
|
|
55
|
+
RuntimeErrorType.HYPOTHESIS_DEADLINE_EXCEEDED: (
|
|
56
|
+
f"Adjust the deadline using {bold('`--hypothesis-deadline=MILLIS`')} or "
|
|
57
|
+
f"disable with {bold('`--hypothesis-deadline=None`')}."
|
|
58
|
+
),
|
|
59
|
+
RuntimeErrorType.HYPOTHESIS_UNSATISFIABLE: "Examine the schema for inconsistencies and consider simplifying it.",
|
|
60
|
+
RuntimeErrorType.SCHEMA_BODY_IN_GET_REQUEST: DISABLE_SCHEMA_VALIDATION_SUGGESTION,
|
|
61
|
+
RuntimeErrorType.SCHEMA_INVALID_REGULAR_EXPRESSION: "Ensure your regex is compatible with Python's syntax.\n"
|
|
62
|
+
"For guidance, visit: https://docs.python.org/3/library/re.html",
|
|
63
|
+
RuntimeErrorType.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Define a custom strategy for it.\n"
|
|
64
|
+
"For guidance, visit: https://schemathesis.readthedocs.io/en/stable/graphql.html#custom-scalars",
|
|
65
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_DATA_TOO_LARGE: _format_health_check_suggestion("data_too_large"),
|
|
66
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_FILTER_TOO_MUCH: _format_health_check_suggestion("filter_too_much"),
|
|
67
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_TOO_SLOW: _format_health_check_suggestion("too_slow"),
|
|
68
|
+
RuntimeErrorType.HYPOTHESIS_HEALTH_CHECK_LARGE_BASE_EXAMPLE: _format_health_check_suggestion(
|
|
69
|
+
"large_base_example"
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
return RUNTIME_ERROR_SUGGESTIONS.get(error_type)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
import random
|
|
3
|
-
from dataclasses import dataclass
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from typing import Union, Iterable
|
|
5
|
+
from typing import Union, Iterable, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from hypothesis.strategies import SearchStrategy
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
class DataGenerationMethod(str, Enum):
|
|
@@ -58,6 +61,13 @@ def generate_random_case_id(length: int = 6) -> str:
|
|
|
58
61
|
return output
|
|
59
62
|
|
|
60
63
|
|
|
64
|
+
@dataclass
|
|
65
|
+
class HeaderConfig:
|
|
66
|
+
"""Configuration for generating headers."""
|
|
67
|
+
|
|
68
|
+
strategy: SearchStrategy[str] | None = None
|
|
69
|
+
|
|
70
|
+
|
|
61
71
|
@dataclass
|
|
62
72
|
class GenerationConfig:
|
|
63
73
|
"""Holds various configuration options relevant for data generation."""
|
|
@@ -66,3 +76,5 @@ class GenerationConfig:
|
|
|
66
76
|
allow_x00: bool = True
|
|
67
77
|
# Generate strings using the given codec
|
|
68
78
|
codec: str | None = "utf-8"
|
|
79
|
+
# Header generation configuration
|
|
80
|
+
headers: HeaderConfig = field(default_factory=HeaderConfig)
|