schemathesis 4.0.0a9__py3-none-any.whl → 4.0.0a11__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 (93) hide show
  1. schemathesis/__init__.py +3 -7
  2. schemathesis/checks.py +17 -7
  3. schemathesis/cli/commands/__init__.py +51 -3
  4. schemathesis/cli/commands/data.py +10 -0
  5. schemathesis/cli/commands/run/__init__.py +147 -260
  6. schemathesis/cli/commands/run/context.py +2 -3
  7. schemathesis/cli/commands/run/events.py +4 -0
  8. schemathesis/cli/commands/run/executor.py +60 -73
  9. schemathesis/cli/commands/run/filters.py +15 -165
  10. schemathesis/cli/commands/run/handlers/cassettes.py +105 -104
  11. schemathesis/cli/commands/run/handlers/junitxml.py +6 -5
  12. schemathesis/cli/commands/run/handlers/output.py +26 -47
  13. schemathesis/cli/commands/run/loaders.py +35 -50
  14. schemathesis/cli/commands/run/validation.py +36 -161
  15. schemathesis/cli/core.py +5 -3
  16. schemathesis/cli/ext/fs.py +7 -5
  17. schemathesis/cli/ext/options.py +0 -21
  18. schemathesis/config/__init__.py +188 -0
  19. schemathesis/config/_auth.py +51 -0
  20. schemathesis/config/_checks.py +268 -0
  21. schemathesis/config/_diff_base.py +99 -0
  22. schemathesis/config/_env.py +21 -0
  23. schemathesis/config/_error.py +156 -0
  24. schemathesis/config/_generation.py +150 -0
  25. schemathesis/config/_health_check.py +24 -0
  26. schemathesis/config/_operations.py +313 -0
  27. schemathesis/config/_output.py +171 -0
  28. schemathesis/config/_parameters.py +19 -0
  29. schemathesis/config/_phases.py +151 -0
  30. schemathesis/config/_projects.py +495 -0
  31. schemathesis/config/_rate_limit.py +17 -0
  32. schemathesis/config/_report.py +116 -0
  33. schemathesis/config/_validator.py +9 -0
  34. schemathesis/config/schema.json +837 -0
  35. schemathesis/core/__init__.py +3 -0
  36. schemathesis/core/compat.py +16 -9
  37. schemathesis/core/errors.py +19 -2
  38. schemathesis/core/failures.py +6 -7
  39. schemathesis/core/hooks.py +20 -0
  40. schemathesis/core/output/__init__.py +14 -37
  41. schemathesis/core/output/sanitization.py +3 -146
  42. schemathesis/core/validation.py +16 -0
  43. schemathesis/engine/__init__.py +2 -4
  44. schemathesis/engine/context.py +41 -43
  45. schemathesis/engine/core.py +7 -5
  46. schemathesis/engine/phases/__init__.py +10 -0
  47. schemathesis/engine/phases/probes.py +8 -8
  48. schemathesis/engine/phases/stateful/_executor.py +68 -43
  49. schemathesis/engine/phases/unit/__init__.py +23 -15
  50. schemathesis/engine/phases/unit/_executor.py +77 -17
  51. schemathesis/engine/phases/unit/_pool.py +1 -1
  52. schemathesis/errors.py +2 -0
  53. schemathesis/filters.py +2 -3
  54. schemathesis/generation/__init__.py +6 -31
  55. schemathesis/generation/case.py +5 -3
  56. schemathesis/generation/coverage.py +174 -134
  57. schemathesis/generation/hypothesis/__init__.py +7 -1
  58. schemathesis/generation/hypothesis/builder.py +40 -14
  59. schemathesis/generation/meta.py +3 -3
  60. schemathesis/generation/overrides.py +37 -1
  61. schemathesis/generation/stateful/state_machine.py +8 -1
  62. schemathesis/graphql/loaders.py +21 -12
  63. schemathesis/openapi/checks.py +12 -8
  64. schemathesis/openapi/generation/filters.py +10 -8
  65. schemathesis/openapi/loaders.py +22 -13
  66. schemathesis/pytest/lazy.py +2 -5
  67. schemathesis/pytest/plugin.py +11 -2
  68. schemathesis/schemas.py +13 -61
  69. schemathesis/specs/graphql/schemas.py +11 -15
  70. schemathesis/specs/openapi/_hypothesis.py +12 -8
  71. schemathesis/specs/openapi/checks.py +16 -18
  72. schemathesis/specs/openapi/examples.py +4 -3
  73. schemathesis/specs/openapi/formats.py +2 -2
  74. schemathesis/specs/openapi/negative/__init__.py +2 -2
  75. schemathesis/specs/openapi/patterns.py +46 -16
  76. schemathesis/specs/openapi/references.py +2 -3
  77. schemathesis/specs/openapi/schemas.py +11 -20
  78. schemathesis/specs/openapi/stateful/__init__.py +10 -5
  79. schemathesis/transport/prepare.py +7 -6
  80. schemathesis/transport/requests.py +3 -1
  81. schemathesis/transport/wsgi.py +3 -4
  82. {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/METADATA +7 -8
  83. schemathesis-4.0.0a11.dist-info/RECORD +166 -0
  84. schemathesis/cli/commands/run/checks.py +0 -79
  85. schemathesis/cli/commands/run/hypothesis.py +0 -78
  86. schemathesis/cli/commands/run/reports.py +0 -72
  87. schemathesis/cli/hooks.py +0 -36
  88. schemathesis/engine/config.py +0 -59
  89. schemathesis/experimental/__init__.py +0 -72
  90. schemathesis-4.0.0a9.dist-info/RECORD +0 -153
  91. {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/WHEEL +0 -0
  92. {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/entry_points.txt +0 -0
  93. {schemathesis-4.0.0a9.dist-info → schemathesis-4.0.0a11.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,495 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, Any, Literal
6
+
7
+ from schemathesis.config._auth import AuthConfig
8
+ from schemathesis.config._checks import ChecksConfig
9
+ from schemathesis.config._diff_base import DiffBase
10
+ from schemathesis.config._env import resolve
11
+ from schemathesis.config._error import ConfigError
12
+ from schemathesis.config._generation import GenerationConfig
13
+ from schemathesis.config._health_check import HealthCheck
14
+ from schemathesis.config._operations import OperationConfig, OperationsConfig
15
+ from schemathesis.config._output import OutputConfig
16
+ from schemathesis.config._parameters import load_parameters
17
+ from schemathesis.config._phases import PhasesConfig
18
+ from schemathesis.config._rate_limit import build_limiter
19
+ from schemathesis.config._report import ReportsConfig
20
+ from schemathesis.core import HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER, hooks
21
+ from schemathesis.core.validation import validate_base_url
22
+
23
+ if TYPE_CHECKING:
24
+ import hypothesis
25
+ from pyrate_limiter import Limiter
26
+
27
+ from schemathesis.config import SchemathesisConfig
28
+ from schemathesis.schemas import APIOperation
29
+
30
+ DEFAULT_WORKERS = 1
31
+
32
+
33
+ def get_workers_count() -> int:
34
+ """Detect the number of available CPUs for the current process, if possible.
35
+
36
+ Use ``DEFAULT_WORKERS`` if not possible to detect.
37
+ """
38
+ if hasattr(os, "sched_getaffinity"):
39
+ # In contrast with `os.cpu_count` this call respects limits on CPU resources on some Unix systems
40
+ return len(os.sched_getaffinity(0))
41
+ # Number of CPUs in the system, or 1 if undetermined
42
+ return os.cpu_count() or DEFAULT_WORKERS
43
+
44
+
45
+ @dataclass(repr=False)
46
+ class ProjectConfig(DiffBase):
47
+ _parent: SchemathesisConfig | None
48
+ base_url: str | None
49
+ headers: dict | None
50
+ hooks: str | None
51
+ proxy: str | None
52
+ workers: int
53
+ continue_on_failure: bool | None
54
+ tls_verify: bool | str | None
55
+ rate_limit: Limiter | None
56
+ request_timeout: float | int | None
57
+ request_cert: str | None
58
+ request_cert_key: str | None
59
+ parameters: dict[str, Any]
60
+ auth: AuthConfig
61
+ checks: ChecksConfig
62
+ phases: PhasesConfig
63
+ generation: GenerationConfig
64
+ operations: OperationsConfig
65
+
66
+ __slots__ = (
67
+ "_parent",
68
+ "base_url",
69
+ "headers",
70
+ "hooks",
71
+ "proxy",
72
+ "workers",
73
+ "continue_on_failure",
74
+ "tls_verify",
75
+ "rate_limit",
76
+ "_rate_limit",
77
+ "request_timeout",
78
+ "request_cert",
79
+ "request_cert_key",
80
+ "parameters",
81
+ "auth",
82
+ "checks",
83
+ "phases",
84
+ "generation",
85
+ "operations",
86
+ )
87
+
88
+ def __init__(
89
+ self,
90
+ *,
91
+ parent: SchemathesisConfig | None = None,
92
+ base_url: str | None = None,
93
+ headers: dict | None = None,
94
+ hooks_: str | None = None,
95
+ workers: int | Literal["auto"] = DEFAULT_WORKERS,
96
+ proxy: str | None = None,
97
+ continue_on_failure: bool | None = None,
98
+ tls_verify: bool | str | None = None,
99
+ rate_limit: str | None = None,
100
+ request_timeout: float | int | None = None,
101
+ request_cert: str | None = None,
102
+ request_cert_key: str | None = None,
103
+ parameters: dict[str, Any] | None = None,
104
+ auth: AuthConfig | None = None,
105
+ checks: ChecksConfig | None = None,
106
+ phases: PhasesConfig | None = None,
107
+ generation: GenerationConfig | None = None,
108
+ operations: OperationsConfig | None = None,
109
+ ) -> None:
110
+ self._parent = parent
111
+ if base_url is not None:
112
+ _validate_base_url(base_url)
113
+ self.base_url = base_url
114
+ self.headers = headers
115
+ self.hooks = hooks_
116
+ if hooks_:
117
+ hooks.load_from_path(hooks_)
118
+ else:
119
+ hooks.load_from_env()
120
+ if isinstance(workers, int):
121
+ self.workers = workers
122
+ else:
123
+ self.workers = get_workers_count()
124
+ self.proxy = proxy
125
+ self.continue_on_failure = continue_on_failure
126
+ self.tls_verify = tls_verify
127
+ if rate_limit is not None:
128
+ self.rate_limit = build_limiter(rate_limit)
129
+ else:
130
+ self.rate_limit = rate_limit
131
+ self._rate_limit = rate_limit
132
+ self.request_timeout = request_timeout
133
+ self.request_cert = request_cert
134
+ self.request_cert_key = request_cert_key
135
+ self.parameters = parameters or {}
136
+ self.auth = auth or AuthConfig()
137
+ self.checks = checks or ChecksConfig()
138
+ self.phases = phases or PhasesConfig()
139
+ self.generation = generation or GenerationConfig()
140
+ self.operations = operations or OperationsConfig()
141
+
142
+ @classmethod
143
+ def from_dict(cls, data: dict[str, Any]) -> ProjectConfig:
144
+ return cls(
145
+ base_url=resolve(data.get("base-url")),
146
+ headers={resolve(key): resolve(value) for key, value in data.get("headers", {}).items()}
147
+ if "headers" in data
148
+ else None,
149
+ hooks_=resolve(data.get("hooks")),
150
+ workers=data.get("workers", DEFAULT_WORKERS),
151
+ proxy=resolve(data.get("proxy")),
152
+ continue_on_failure=data.get("continue-on-failure", None),
153
+ tls_verify=resolve(data.get("tls-verify")),
154
+ rate_limit=resolve(data.get("rate-limit")),
155
+ request_timeout=data.get("request-timeout"),
156
+ request_cert=resolve(data.get("request-cert")),
157
+ request_cert_key=resolve(data.get("request-cert-key")),
158
+ parameters=load_parameters(data),
159
+ auth=AuthConfig.from_dict(data.get("auth", {})),
160
+ checks=ChecksConfig.from_dict(data.get("checks", {})),
161
+ phases=PhasesConfig.from_dict(data.get("phases", {})),
162
+ generation=GenerationConfig.from_dict(data.get("generation", {})),
163
+ operations=OperationsConfig(
164
+ operations=[OperationConfig.from_dict(operation) for operation in data.get("operations", [])]
165
+ ),
166
+ )
167
+
168
+ def update(
169
+ self,
170
+ *,
171
+ base_url: str | None = None,
172
+ headers: dict | None = None,
173
+ basic_auth: tuple[str, str] | None = None,
174
+ workers: int | Literal["auto"] | None = None,
175
+ continue_on_failure: bool | None = None,
176
+ rate_limit: str | None = None,
177
+ request_timeout: float | int | None = None,
178
+ tls_verify: bool | str | None = None,
179
+ request_cert: str | None = None,
180
+ request_cert_key: str | None = None,
181
+ proxy: str | None = None,
182
+ suppress_health_check: list[HealthCheck] | None = None,
183
+ ) -> None:
184
+ if base_url is not None:
185
+ _validate_base_url(base_url)
186
+ self.base_url = base_url
187
+
188
+ if headers is not None:
189
+ _headers = self.headers or {}
190
+ _headers.update(headers)
191
+ self.headers = _headers
192
+
193
+ if basic_auth is not None:
194
+ self.auth.update(basic=basic_auth)
195
+
196
+ if workers is not None:
197
+ if isinstance(workers, int):
198
+ self.workers = workers
199
+ else:
200
+ self.workers = get_workers_count()
201
+
202
+ if continue_on_failure is not None:
203
+ self.continue_on_failure = continue_on_failure
204
+
205
+ if rate_limit is not None:
206
+ self.rate_limit = build_limiter(rate_limit)
207
+
208
+ if request_timeout is not None:
209
+ self.request_timeout = request_timeout
210
+
211
+ if tls_verify is not None:
212
+ self.tls_verify = tls_verify
213
+
214
+ if request_cert is not None:
215
+ self.request_cert = request_cert
216
+
217
+ if request_cert_key is not None:
218
+ self.request_cert_key = request_cert_key
219
+
220
+ if proxy is not None:
221
+ self.proxy = proxy
222
+
223
+ if suppress_health_check is not None:
224
+ self.suppress_health_check = suppress_health_check
225
+
226
+ def auth_for(self, *, operation: APIOperation | None = None) -> tuple[str, str] | None:
227
+ """Get auth credentials, prioritizing operation-specific configs."""
228
+ if operation is not None:
229
+ config = self.operations.get_for_operation(operation=operation)
230
+ if config.auth.basic is not None:
231
+ return config.auth.basic
232
+ if self.auth.basic is not None:
233
+ return self.auth.basic
234
+ return None
235
+
236
+ def headers_for(self, *, operation: APIOperation | None = None) -> dict[str, str] | None:
237
+ """Get explicitly configured headers."""
238
+ headers = self.headers.copy() if self.headers else {}
239
+ if operation is not None:
240
+ config = self.operations.get_for_operation(operation=operation)
241
+ if config.headers is not None:
242
+ headers.update(config.headers)
243
+ return headers
244
+
245
+ def request_timeout_for(self, *, operation: APIOperation | None = None) -> float | int | None:
246
+ if operation is not None:
247
+ config = self.operations.get_for_operation(operation=operation)
248
+ if config.request_timeout is not None:
249
+ return config.request_timeout
250
+ if self.request_timeout is not None:
251
+ return self.request_timeout
252
+ return None
253
+
254
+ def tls_verify_for(self, *, operation: APIOperation | None = None) -> bool | str | None:
255
+ if operation is not None:
256
+ config = self.operations.get_for_operation(operation=operation)
257
+ if config.tls_verify is not None:
258
+ return config.tls_verify
259
+ if self.tls_verify is not None:
260
+ return self.tls_verify
261
+ return None
262
+
263
+ def request_cert_for(self, *, operation: APIOperation | None = None) -> str | tuple[str, str] | None:
264
+ if operation is not None:
265
+ config = self.operations.get_for_operation(operation=operation)
266
+ if config.request_cert is not None:
267
+ if config.request_cert_key:
268
+ return (config.request_cert, config.request_cert_key)
269
+ return config.request_cert
270
+ if self.request_cert is not None:
271
+ if self.request_cert_key:
272
+ return (self.request_cert, self.request_cert_key)
273
+ return self.request_cert
274
+ return None
275
+
276
+ def proxy_for(self, *, operation: APIOperation | None = None) -> str | None:
277
+ if operation is not None:
278
+ config = self.operations.get_for_operation(operation=operation)
279
+ if config.proxy is not None:
280
+ return config.proxy
281
+ if self.proxy is not None:
282
+ return self.proxy
283
+ return None
284
+
285
+ def rate_limit_for(self, *, operation: APIOperation | None = None) -> Limiter | None:
286
+ if operation is not None:
287
+ config = self.operations.get_for_operation(operation=operation)
288
+ if config.rate_limit is not None:
289
+ return config.rate_limit
290
+ if self.rate_limit is not None:
291
+ return self.rate_limit
292
+ return None
293
+
294
+ def phases_for(self, *, operation: APIOperation | None) -> PhasesConfig:
295
+ configs = []
296
+ if operation is not None:
297
+ for op in self.operations.operations:
298
+ if op._filter_set.applies_to(operation=operation):
299
+ configs.append(op.phases)
300
+ configs.append(self.phases)
301
+ return PhasesConfig.from_hierarchy(configs)
302
+
303
+ def generation_for(
304
+ self,
305
+ *,
306
+ operation: APIOperation | None = None,
307
+ phase: str | None = None,
308
+ ) -> GenerationConfig:
309
+ configs = []
310
+ if operation is not None:
311
+ for op in self.operations.operations:
312
+ if op._filter_set.applies_to(operation=operation):
313
+ if phase is not None:
314
+ phase_config = op.phases.get_by_name(name=phase)
315
+ configs.append(phase_config.generation)
316
+ configs.append(op.generation)
317
+ if phase is not None:
318
+ phases = self.phases_for(operation=operation)
319
+ phase_config = phases.get_by_name(name=phase)
320
+ configs.append(phase_config.generation)
321
+ configs.append(self.generation)
322
+ return GenerationConfig.from_hierarchy(configs)
323
+
324
+ def checks_config_for(
325
+ self,
326
+ *,
327
+ operation: APIOperation | None = None,
328
+ phase: str | None = None,
329
+ ) -> ChecksConfig:
330
+ configs = []
331
+ if operation is not None:
332
+ for op in self.operations.operations:
333
+ if op._filter_set.applies_to(operation=operation):
334
+ if phase is not None:
335
+ phase_config = op.phases.get_by_name(name=phase)
336
+ configs.append(phase_config.checks)
337
+ configs.append(op.checks)
338
+ if phase is not None:
339
+ phases = self.phases_for(operation=operation)
340
+ phase_config = phases.get_by_name(name=phase)
341
+ configs.append(phase_config.checks)
342
+ configs.append(self.checks)
343
+ return ChecksConfig.from_hierarchy(configs)
344
+
345
+ def get_hypothesis_settings(
346
+ self,
347
+ *,
348
+ operation: APIOperation | None = None,
349
+ phase: str | None = None,
350
+ ) -> hypothesis.settings:
351
+ import hypothesis
352
+ from hypothesis.database import DirectoryBasedExampleDatabase, InMemoryExampleDatabase
353
+
354
+ config = self.generation_for(operation=operation, phase=phase)
355
+ kwargs: dict[str, Any] = {}
356
+
357
+ if config.max_examples is not None:
358
+ kwargs["max_examples"] = config.max_examples
359
+ phases = set(hypothesis.Phase) - {hypothesis.Phase.explain}
360
+ if config.no_shrink:
361
+ phases.discard(hypothesis.Phase.shrink)
362
+ database = config.database
363
+ if database is not None:
364
+ if database.lower() == "none":
365
+ kwargs["database"] = None
366
+ phases.discard(hypothesis.Phase.reuse)
367
+ elif database == HYPOTHESIS_IN_MEMORY_DATABASE_IDENTIFIER:
368
+ kwargs["database"] = InMemoryExampleDatabase()
369
+ else:
370
+ kwargs["database"] = DirectoryBasedExampleDatabase(database)
371
+
372
+ return hypothesis.settings(
373
+ derandomize=config.deterministic,
374
+ print_blob=False,
375
+ deadline=None,
376
+ verbosity=hypothesis.Verbosity.quiet,
377
+ suppress_health_check=[check for item in self.suppress_health_check for check in item.as_hypothesis()],
378
+ phases=phases,
379
+ # NOTE: Ignoring any operation-specific config as stateful tests are not operation-specific
380
+ stateful_step_count=self.phases.stateful.max_steps,
381
+ **kwargs,
382
+ )
383
+
384
+ def _get_parent(self) -> SchemathesisConfig:
385
+ if self._parent is None:
386
+ from schemathesis.config import SchemathesisConfig
387
+
388
+ self._parent = SchemathesisConfig.discover()
389
+ return self._parent
390
+
391
+ @property
392
+ def output(self) -> OutputConfig:
393
+ return self._get_parent().output
394
+
395
+ @property
396
+ def wait_for_schema(self) -> float | int | None:
397
+ return self._get_parent().wait_for_schema
398
+
399
+ @property
400
+ def max_failures(self) -> int | None:
401
+ return self._get_parent().max_failures
402
+
403
+ @max_failures.setter
404
+ def max_failures(self, value: int) -> None:
405
+ parent = self._get_parent()
406
+ parent.max_failures = value
407
+
408
+ @property
409
+ def reports(self) -> ReportsConfig:
410
+ return self._get_parent().reports
411
+
412
+ @property
413
+ def suppress_health_check(self) -> list[HealthCheck]:
414
+ return self._get_parent().suppress_health_check
415
+
416
+ @suppress_health_check.setter
417
+ def suppress_health_check(self, value: list[HealthCheck]) -> None:
418
+ parent = self._get_parent()
419
+ parent.suppress_health_check = value
420
+
421
+ @property
422
+ def seed(self) -> int:
423
+ return self._get_parent().seed
424
+
425
+ @seed.setter
426
+ def seed(self, value: int) -> None:
427
+ parent = self._get_parent()
428
+ parent._seed = value
429
+
430
+
431
+ def _validate_base_url(base_url: str) -> None:
432
+ try:
433
+ validate_base_url(base_url)
434
+ except ValueError as exc:
435
+ raise ConfigError(str(exc)) from None
436
+
437
+
438
+ @dataclass(repr=False)
439
+ class ProjectsConfig(DiffBase):
440
+ default: ProjectConfig
441
+ named: dict[str, ProjectConfig]
442
+ _override: ProjectConfig
443
+
444
+ __slots__ = ("default", "named", "_override")
445
+
446
+ def __init__(
447
+ self,
448
+ *,
449
+ default: ProjectConfig | None = None,
450
+ named: dict[str, ProjectConfig] | None = None,
451
+ ) -> None:
452
+ self.default = default or ProjectConfig()
453
+ self.named = named or {}
454
+
455
+ @property
456
+ def override(self) -> ProjectConfig:
457
+ if not hasattr(self, "_override"):
458
+ self._override = ProjectConfig()
459
+ self._override._parent = self.default._parent
460
+ return self._override
461
+
462
+ @classmethod
463
+ def from_dict(cls, data: dict[str, Any]) -> ProjectsConfig:
464
+ return cls(
465
+ default=ProjectConfig.from_dict(data),
466
+ named={project["title"]: ProjectConfig.from_dict(project) for project in data.get("project", [])},
467
+ )
468
+
469
+ def _set_parent(self, parent: SchemathesisConfig) -> None:
470
+ self.default._parent = parent
471
+ for project in self.named.values():
472
+ project._parent = parent
473
+
474
+ def get_default(self) -> ProjectConfig:
475
+ config = ProjectConfig.from_hierarchy([self.override, self.default])
476
+ config._parent = self.default._parent
477
+ return config
478
+
479
+ def get(self, schema: dict[str, Any]) -> ProjectConfig:
480
+ # Highest priority goes to `override`, then config specifically
481
+ # for the given project, then the "default" project config
482
+ configs = []
483
+ if hasattr(self, "_override"):
484
+ configs.append(self._override)
485
+ title = schema.get("info", {}).get("title")
486
+ if title is not None:
487
+ named = self.named.get(title)
488
+ if named is not None:
489
+ configs.append(named)
490
+ if not configs:
491
+ return self.default
492
+ configs.append(self.default)
493
+ config = ProjectConfig.from_hierarchy(configs)
494
+ config._parent = self.default._parent
495
+ return config
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from schemathesis.config._error import ConfigError
6
+ from schemathesis.core import rate_limit
7
+ from schemathesis.core.errors import InvalidRateLimit
8
+
9
+ if TYPE_CHECKING:
10
+ from pyrate_limiter import Limiter
11
+
12
+
13
+ def build_limiter(value: str) -> Limiter:
14
+ try:
15
+ return rate_limit.build_limiter(value)
16
+ except InvalidRateLimit as exc:
17
+ raise ConfigError(str(exc)) from None
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from schemathesis.config._diff_base import DiffBase
9
+ from schemathesis.config._env import resolve
10
+
11
+ DEFAULT_REPORT_DIRECTORY = Path("./schemathesis-report")
12
+
13
+
14
+ class ReportFormat(str, Enum):
15
+ """Available report formats."""
16
+
17
+ JUNIT = "junit"
18
+ VCR = "vcr"
19
+ HAR = "har"
20
+
21
+ @property
22
+ def extension(self) -> str:
23
+ """File extension for this format."""
24
+ return {
25
+ self.JUNIT: "xml",
26
+ self.VCR: "yaml",
27
+ self.HAR: "json",
28
+ }[self]
29
+
30
+
31
+ @dataclass(repr=False)
32
+ class ReportConfig(DiffBase):
33
+ enabled: bool
34
+ path: Path | None
35
+
36
+ __slots__ = ("enabled", "path")
37
+
38
+ def __init__(self, *, enabled: bool = False, path: Path | None = None) -> None:
39
+ self.enabled = enabled
40
+ self.path = path
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: dict[str, Any]) -> ReportConfig:
44
+ path = resolve(data.get("path"))
45
+ if path is not None:
46
+ return cls(enabled=True, path=Path(path))
47
+ enabled = data.get("enabled", False)
48
+ return cls(enabled=enabled, path=path)
49
+
50
+
51
+ @dataclass(repr=False)
52
+ class ReportsConfig(DiffBase):
53
+ directory: Path
54
+ preserve_bytes: bool
55
+ junit: ReportConfig
56
+ vcr: ReportConfig
57
+ har: ReportConfig
58
+
59
+ __slots__ = ("directory", "preserve_bytes", "junit", "vcr", "har")
60
+
61
+ def __init__(
62
+ self,
63
+ *,
64
+ directory: str | None = None,
65
+ preserve_bytes: bool = False,
66
+ junit: ReportConfig | None = None,
67
+ vcr: ReportConfig | None = None,
68
+ har: ReportConfig | None = None,
69
+ ) -> None:
70
+ self.directory = Path(resolve(directory) or DEFAULT_REPORT_DIRECTORY)
71
+ self.preserve_bytes = preserve_bytes
72
+ self.junit = junit or ReportConfig()
73
+ self.vcr = vcr or ReportConfig()
74
+ self.har = har or ReportConfig()
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: dict[str, Any]) -> ReportsConfig:
78
+ return cls(
79
+ directory=data.get("directory"),
80
+ preserve_bytes=data.get("preserve-bytes", False),
81
+ junit=ReportConfig.from_dict(data.get("junit", {})),
82
+ vcr=ReportConfig.from_dict(data.get("vcr", {})),
83
+ har=ReportConfig.from_dict(data.get("har", {})),
84
+ )
85
+
86
+ def update(
87
+ self,
88
+ *,
89
+ formats: list[ReportFormat] | None = None,
90
+ junit_path: str | None = None,
91
+ vcr_path: str | None = None,
92
+ har_path: str | None = None,
93
+ directory: Path = DEFAULT_REPORT_DIRECTORY,
94
+ preserve_bytes: bool = False,
95
+ ) -> None:
96
+ formats = formats or []
97
+ if junit_path is not None or ReportFormat.JUNIT in formats:
98
+ self.junit.enabled = True
99
+ self.junit.path = Path(junit_path) if junit_path is not None else junit_path
100
+ if vcr_path is not None or ReportFormat.VCR in formats:
101
+ self.vcr.enabled = True
102
+ self.vcr.path = Path(vcr_path) if vcr_path is not None else vcr_path
103
+ if har_path is not None or ReportFormat.HAR in formats:
104
+ self.har.enabled = True
105
+ self.har.path = Path(har_path) if har_path is not None else har_path
106
+ if directory != DEFAULT_REPORT_DIRECTORY:
107
+ self.directory = directory
108
+ if preserve_bytes is True:
109
+ self.preserve_bytes = preserve_bytes
110
+
111
+ def get_path(self, format: ReportFormat) -> Path:
112
+ """Get the final path for a specific format."""
113
+ report: ReportConfig = getattr(self, format.value)
114
+ if report.path is not None:
115
+ return report.path
116
+ return self.directory / f"{format.value}.{format.extension}"
@@ -0,0 +1,9 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import jsonschema.validators
5
+
6
+ with (Path(__file__).absolute().parent / "schema.json").open() as fd:
7
+ CONFIG_SCHEMA = json.loads(fd.read())
8
+
9
+ CONFIG_VALIDATOR = jsonschema.validators.Draft202012Validator(CONFIG_SCHEMA)