schemathesis 3.35.1__py3-none-any.whl → 3.35.3__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.
@@ -4,7 +4,7 @@ import os
4
4
  import shutil
5
5
  from dataclasses import dataclass, field
6
6
  from queue import Queue
7
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, Generator
8
8
 
9
9
  from ..code_samples import CodeSampleStyle
10
10
  from ..internal.deprecation import deprecated_property
@@ -60,11 +60,15 @@ class ExecutionContext:
60
60
  analysis: Result[AnalysisResult, Exception] | None = None
61
61
  output_config: OutputConfig = field(default_factory=OutputConfig)
62
62
  state_machine_sink: StateMachineSink | None = None
63
- summary_lines: list[str] = field(default_factory=list)
63
+ initialization_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
64
+ summary_lines: list[str | Generator[str, None, None]] = field(default_factory=list)
64
65
 
65
66
  @deprecated_property(removed_in="4.0", replacement="show_trace")
66
67
  def show_errors_tracebacks(self) -> bool:
67
68
  return self.show_trace
68
69
 
69
- def add_summary_line(self, line: str) -> None:
70
+ def add_initialization_line(self, line: str | Generator[str, None, None]) -> None:
71
+ self.initialization_lines.append(line)
72
+
73
+ def add_summary_line(self, line: str | Generator[str, None, None]) -> None:
70
74
  self.summary_lines.append(line)
@@ -7,6 +7,7 @@ import textwrap
7
7
  import time
8
8
  from importlib import metadata
9
9
  from queue import Queue
10
+ from types import GeneratorType
10
11
  from typing import TYPE_CHECKING, Any, Generator, Literal, cast
11
12
 
12
13
  import click
@@ -776,6 +777,8 @@ def handle_initialized(context: ExecutionContext, event: events.Initialized) ->
776
777
  click.secho(f"Collected API links: {links_count}", bold=True)
777
778
  if isinstance(context.report, ServiceReportContext):
778
779
  click.secho("Report to Schemathesis.io: ENABLED", bold=True)
780
+ if context.initialization_lines:
781
+ _print_lines(context.initialization_lines)
779
782
 
780
783
 
781
784
  def handle_before_probing(context: ExecutionContext, event: events.BeforeProbing) -> None:
@@ -852,12 +855,20 @@ def handle_finished(context: ExecutionContext, event: events.Finished) -> None:
852
855
  display_statistic(context, event)
853
856
  if context.summary_lines:
854
857
  click.echo()
855
- for line in context.summary_lines:
856
- click.echo(line)
858
+ _print_lines(context.summary_lines)
857
859
  click.echo()
858
860
  display_summary(event)
859
861
 
860
862
 
863
+ def _print_lines(lines: list[str | Generator[str, None, None]]) -> None:
864
+ for entry in lines:
865
+ if isinstance(entry, str):
866
+ click.echo(entry)
867
+ elif isinstance(entry, GeneratorType):
868
+ for line in entry:
869
+ click.echo(line)
870
+
871
+
861
872
  def handle_interrupted(context: ExecutionContext, event: events.Interrupted) -> None:
862
873
  click.echo()
863
874
  _handle_interrupted(context)
@@ -201,12 +201,12 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
201
201
  seen.add(value)
202
202
  elif key == "multipleOf":
203
203
  yield from _negative_multiple_of(ctx, schema, value)
204
- elif key == "minLength" and 0 < value < BUFFER_SIZE and "pattern" not in schema:
204
+ elif key == "minLength" and 0 < value < BUFFER_SIZE:
205
205
  with suppress(InvalidArgument):
206
206
  yield NegativeValue(
207
207
  ctx.generate_from_schema({**schema, "minLength": value - 1, "maxLength": value - 1})
208
208
  )
209
- elif key == "maxLength" and value < BUFFER_SIZE and "pattern" not in schema:
209
+ elif key == "maxLength" and value < BUFFER_SIZE:
210
210
  with suppress(InvalidArgument):
211
211
  yield NegativeValue(
212
212
  ctx.generate_from_schema({**schema, "minLength": value + 1, "maxLength": value + 1})
@@ -234,6 +234,17 @@ def cover_schema_iter(ctx: CoverageContext, schema: dict | bool) -> Generator[Ge
234
234
  yield from cover_schema_iter(nctx, sub_schema)
235
235
 
236
236
 
237
+ def _get_properties(schema: dict | bool) -> dict | bool:
238
+ if isinstance(schema, dict):
239
+ if "example" in schema:
240
+ return {"const": schema["example"]}
241
+ if "examples" in schema and schema["examples"]:
242
+ return {"enum": schema["examples"]}
243
+ if schema.get("type") == "object":
244
+ return _get_template_schema(schema, "object")
245
+ return schema
246
+
247
+
237
248
  def _get_template_schema(schema: dict, ty: str) -> dict:
238
249
  if ty == "object":
239
250
  properties = schema.get("properties")
@@ -242,10 +253,7 @@ def _get_template_schema(schema: dict, ty: str) -> dict:
242
253
  **schema,
243
254
  "required": list(properties),
244
255
  "type": ty,
245
- "properties": {
246
- k: _get_template_schema(v, "object") if isinstance(v, dict) and v.get("type") == "object" else v
247
- for k, v in properties.items()
248
- },
256
+ "properties": {k: _get_properties(v) for k, v in properties.items()},
249
257
  }
250
258
  return {**schema, "type": ty}
251
259
 
@@ -255,14 +263,21 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
255
263
  # Boundary and near boundary values
256
264
  min_length = schema.get("minLength")
257
265
  max_length = schema.get("maxLength")
258
-
259
- if not min_length and not max_length:
266
+ example = schema.get("example")
267
+ examples = schema.get("examples")
268
+ if example or examples:
269
+ if example:
270
+ yield PositiveValue(example)
271
+ if examples:
272
+ for example in examples:
273
+ yield PositiveValue(example)
274
+ elif not min_length and not max_length:
260
275
  # Default positive value
261
276
  yield PositiveValue(ctx.generate_from_schema(schema))
262
277
 
263
278
  seen = set()
264
279
 
265
- if min_length is not None and min_length < BUFFER_SIZE and "pattern" not in schema:
280
+ if min_length is not None and min_length < BUFFER_SIZE:
266
281
  # Exactly the minimum length
267
282
  yield PositiveValue(ctx.generate_from_schema({**schema, "maxLength": min_length}))
268
283
  seen.add(min_length)
@@ -273,7 +288,7 @@ def _positive_string(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
273
288
  yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": larger, "maxLength": larger}))
274
289
  seen.add(larger)
275
290
 
276
- if max_length is not None and "pattern" not in schema:
291
+ if max_length is not None:
277
292
  # Exactly the maximum length
278
293
  if max_length < BUFFER_SIZE and max_length not in seen:
279
294
  yield PositiveValue(ctx.generate_from_schema({**schema, "minLength": max_length}))
@@ -310,8 +325,16 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
310
325
  if exclusive_maximum is not None:
311
326
  maximum = exclusive_maximum - 1
312
327
  multiple_of = schema.get("multipleOf")
313
-
314
- if not minimum and not maximum:
328
+ example = schema.get("example")
329
+ examples = schema.get("examples")
330
+
331
+ if example or examples:
332
+ if example:
333
+ yield PositiveValue(example)
334
+ if examples:
335
+ for example in examples:
336
+ yield PositiveValue(example)
337
+ elif not minimum and not maximum:
315
338
  # Default positive value
316
339
  yield PositiveValue(ctx.generate_from_schema(schema))
317
340
 
@@ -357,7 +380,17 @@ def _positive_number(ctx: CoverageContext, schema: dict) -> Generator[GeneratedV
357
380
 
358
381
  def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Generator[GeneratedValue, None, None]:
359
382
  seen = set()
360
- yield PositiveValue(template)
383
+ example = schema.get("example")
384
+ examples = schema.get("examples")
385
+
386
+ if example or examples:
387
+ if example:
388
+ yield PositiveValue(example)
389
+ if examples:
390
+ for example in examples:
391
+ yield PositiveValue(example)
392
+ else:
393
+ yield PositiveValue(template)
361
394
  seen.add(len(template))
362
395
 
363
396
  # Boundary and near-boundary sizes
@@ -390,7 +423,17 @@ def _positive_array(ctx: CoverageContext, schema: dict, template: list) -> Gener
390
423
 
391
424
 
392
425
  def _positive_object(ctx: CoverageContext, schema: dict, template: dict) -> Generator[GeneratedValue, None, None]:
393
- yield PositiveValue(template)
426
+ example = schema.get("example")
427
+ examples = schema.get("examples")
428
+
429
+ if example or examples:
430
+ if example:
431
+ yield PositiveValue(example)
432
+ if examples:
433
+ for example in examples:
434
+ yield PositiveValue(example)
435
+ else:
436
+ yield PositiveValue(template)
394
437
  # Only required properties
395
438
  properties = schema.get("properties", {})
396
439
  if set(properties) != set(schema.get("required", {})):
schemathesis/models.py CHANGED
@@ -71,6 +71,14 @@ if TYPE_CHECKING:
71
71
  from .transports.responses import GenericResponse, WSGIResponse
72
72
 
73
73
 
74
+ @dataclass
75
+ class TransitionId:
76
+ name: str
77
+ status_code: str
78
+
79
+ __slots__ = ("name", "status_code")
80
+
81
+
74
82
  @dataclass
75
83
  class CaseSource:
76
84
  """Data sources, used to generate a test case."""
@@ -79,6 +87,7 @@ class CaseSource:
79
87
  response: GenericResponse
80
88
  elapsed: float
81
89
  overrides_all_parameters: bool
90
+ transition_id: TransitionId
82
91
 
83
92
  def partial_deepcopy(self) -> CaseSource:
84
93
  return self.__class__(
@@ -86,6 +95,7 @@ class CaseSource:
86
95
  response=self.response,
87
96
  elapsed=self.elapsed,
88
97
  overrides_all_parameters=self.overrides_all_parameters,
98
+ transition_id=self.transition_id,
89
99
  )
90
100
 
91
101
 
@@ -214,9 +224,20 @@ class Case:
214
224
  def app(self) -> Any:
215
225
  return self.operation.app
216
226
 
217
- def set_source(self, response: GenericResponse, case: Case, elapsed: float, overrides_all_parameters: bool) -> None:
227
+ def set_source(
228
+ self,
229
+ response: GenericResponse,
230
+ case: Case,
231
+ elapsed: float,
232
+ overrides_all_parameters: bool,
233
+ transition_id: TransitionId,
234
+ ) -> None:
218
235
  self.source = CaseSource(
219
- case=case, response=response, elapsed=elapsed, overrides_all_parameters=overrides_all_parameters
236
+ case=case,
237
+ response=response,
238
+ elapsed=elapsed,
239
+ overrides_all_parameters=overrides_all_parameters,
240
+ transition_id=transition_id,
220
241
  )
221
242
 
222
243
  @property
@@ -14,7 +14,7 @@ from .serialization import SerializedError, SerializedTestResult
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from ..models import APIOperation, Status, TestResult, TestResultSet
17
- from ..schemas import BaseSchema
17
+ from ..schemas import BaseSchema, Specification
18
18
  from ..service.models import AnalysisResult
19
19
  from ..stateful import events
20
20
  from . import probes
@@ -39,6 +39,7 @@ class Initialized(ExecutionEvent):
39
39
  """Runner is initialized, settings are prepared, requests session is ready."""
40
40
 
41
41
  schema: dict[str, Any]
42
+ specification: Specification
42
43
  # Total number of operations in the schema
43
44
  operations_count: int | None
44
45
  # Total number of links in the schema
@@ -48,6 +49,8 @@ class Initialized(ExecutionEvent):
48
49
  seed: int | None
49
50
  # The base URL against which the tests are running
50
51
  base_url: str
52
+ # The base path part of every operation
53
+ base_path: str
51
54
  # API schema specification name
52
55
  specification_name: str
53
56
  # Monotonic clock value when the test run started. Used to properly calculate run duration, since this clock
@@ -71,10 +74,12 @@ class Initialized(ExecutionEvent):
71
74
  """Computes all needed data from a schema instance."""
72
75
  return cls(
73
76
  schema=schema.raw_schema,
77
+ specification=schema.specification,
74
78
  operations_count=schema.operations_count if count_operations else None,
75
79
  links_count=schema.links_count if count_links else None,
76
80
  location=schema.location,
77
81
  base_url=schema.get_base_url(),
82
+ base_path=schema.base_path,
78
83
  start_time=start_time or time.monotonic(),
79
84
  started_at=started_at or current_datetime(),
80
85
  specification_name=schema.verbose_name,
@@ -306,13 +306,12 @@ class BaseRunner:
306
306
  if isinstance(stateful_event, stateful_events.SuiteFinished):
307
307
  if stateful_event.failures and status != Status.error:
308
308
  status = Status.failure
309
- for failure in stateful_event.failures:
310
- result.checks.append(failure)
311
309
  elif isinstance(stateful_event, stateful_events.RunStarted):
312
310
  test_start_time = stateful_event.timestamp
313
311
  elif isinstance(stateful_event, stateful_events.RunFinished):
314
312
  test_elapsed_time = stateful_event.timestamp - cast(float, test_start_time)
315
313
  elif isinstance(stateful_event, stateful_events.StepFinished):
314
+ result.checks.extend(stateful_event.checks)
316
315
  on_step_finished(stateful_event)
317
316
  elif isinstance(stateful_event, stateful_events.Errored):
318
317
  status = Status.error
@@ -28,7 +28,7 @@ from ..exceptions import (
28
28
  make_unique_by_key,
29
29
  )
30
30
  from ..generation import DataGenerationMethod
31
- from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult
31
+ from ..models import Case, Check, Interaction, Request, Response, Status, TestPhase, TestResult, TransitionId
32
32
  from ..transports import deserialize_payload, serialize_payload
33
33
 
34
34
  if TYPE_CHECKING:
@@ -52,7 +52,9 @@ class SerializedCase:
52
52
  method: str
53
53
  url: str
54
54
  path_template: str
55
+ full_path: str
55
56
  verbose_name: str
57
+ transition_id: TransitionId | None
56
58
  # Transport info
57
59
  verify: bool
58
60
  # Headers coming from sources outside data generation
@@ -78,7 +80,9 @@ class SerializedCase:
78
80
  method=case.method,
79
81
  url=request_data.url,
80
82
  path_template=case.path,
83
+ full_path=case.full_path,
81
84
  verbose_name=case.operation.verbose_name,
85
+ transition_id=case.source.transition_id if case.source is not None else None,
82
86
  verify=verify,
83
87
  extra_headers=request_data.headers,
84
88
  )
@@ -174,7 +178,11 @@ class SerializedCheck:
174
178
 
175
179
 
176
180
  def _get_headers(headers: dict[str, Any] | CaseInsensitiveDict) -> dict[str, str]:
177
- return {key: value[0] for key, value in headers.items() if key not in get_excluded_headers()}
181
+ return {
182
+ key: value[0] if isinstance(value, list) else value
183
+ for key, value in headers.items()
184
+ if key not in get_excluded_headers()
185
+ }
178
186
 
179
187
 
180
188
  @dataclass
schemathesis/schemas.py CHANGED
@@ -1,12 +1,3 @@
1
- """Schema objects provide a convenient interface to raw schemas.
2
-
3
- Their responsibilities:
4
- - Provide a unified way to work with different types of schemas
5
- - Give all paths / methods combinations that are available directly from the schema;
6
-
7
- They give only static definitions of paths.
8
- """
9
-
10
1
  from __future__ import annotations
11
2
 
12
3
  from collections.abc import Mapping
@@ -69,6 +60,7 @@ from .types import (
69
60
  NotSet,
70
61
  PathParameters,
71
62
  Query,
63
+ Specification,
72
64
  )
73
65
  from .utils import PARAMETRIZE_MARKER, GivenInput, given_proxy
74
66
 
@@ -89,6 +81,7 @@ def get_full_path(base_path: str, path: str) -> str:
89
81
  class BaseSchema(Mapping):
90
82
  raw_schema: dict[str, Any]
91
83
  transport: Transport
84
+ specification: Specification
92
85
  location: str | None = None
93
86
  base_url: str | None = None
94
87
  filter_set: FilterSet = field(default_factory=FilterSet)
@@ -408,6 +401,7 @@ class BaseSchema(Mapping):
408
401
 
409
402
  return self.__class__(
410
403
  self.raw_schema,
404
+ specification=self.specification,
411
405
  location=self.location,
412
406
  base_url=base_url, # type: ignore
413
407
  app=app,
@@ -21,7 +21,7 @@ from ...internal.validation import require_relative_url
21
21
  from ...loaders import load_schema_from_url
22
22
  from ...throttling import build_limiter
23
23
  from ...transports.headers import setup_default_headers
24
- from ...types import PathLike
24
+ from ...types import PathLike, Specification
25
25
 
26
26
  if TYPE_CHECKING:
27
27
  from graphql import DocumentNode
@@ -254,6 +254,7 @@ def from_dict(
254
254
  rate_limiter = build_limiter(rate_limit)
255
255
  instance = GraphQLSchema(
256
256
  raw_schema,
257
+ specification=Specification.GRAPHQL,
257
258
  location=location,
258
259
  base_url=base_url,
259
260
  app=app,
@@ -14,7 +14,7 @@ from jsonschema import RefResolver
14
14
 
15
15
  from ...constants import NOT_SET
16
16
  from ...internal.copy import fast_deepcopy
17
- from ...models import APIOperation, Case
17
+ from ...models import APIOperation, Case, TransitionId
18
18
  from ...parameters import ParameterSet
19
19
  from ...stateful import ParsedData, StatefulTest, UnresolvableLink
20
20
  from ...stateful.state_machine import Direction
@@ -226,7 +226,16 @@ class OpenAPILink(Direction):
226
226
  if parameter.name not in overrides.get(parameter.location, []):
227
227
  overrides_all_parameters = False
228
228
  break
229
- case.set_source(context.response, context.case, elapsed, overrides_all_parameters)
229
+ case.set_source(
230
+ context.response,
231
+ context.case,
232
+ elapsed,
233
+ overrides_all_parameters,
234
+ transition_id=TransitionId(
235
+ name=self.name,
236
+ status_code=self.status_code,
237
+ ),
238
+ )
230
239
 
231
240
  def set_parameters(
232
241
  self, case: Case, context: expressions.ExpressionContext
@@ -26,7 +26,7 @@ from ...loaders import load_schema_from_url, load_yaml
26
26
  from ...throttling import build_limiter
27
27
  from ...transports.content_types import is_json_media_type, is_yaml_media_type
28
28
  from ...transports.headers import setup_default_headers
29
- from ...types import Filter, NotSet, PathLike
29
+ from ...types import Filter, NotSet, PathLike, Specification
30
30
  from . import definitions, validation
31
31
 
32
32
  if TYPE_CHECKING:
@@ -349,6 +349,7 @@ def from_dict(
349
349
  _maybe_validate_schema(raw_schema, definitions.SWAGGER_20_VALIDATOR, validate_schema)
350
350
  instance = SwaggerV20(
351
351
  raw_schema,
352
+ specification=Specification.OPENAPI,
352
353
  app=app,
353
354
  base_url=base_url,
354
355
  filter_set=filter_set,
@@ -388,6 +389,7 @@ def from_dict(
388
389
  _maybe_validate_schema(raw_schema, validator, validate_schema)
389
390
  instance = OpenApi30(
390
391
  raw_schema,
392
+ specification=Specification.OPENAPI,
391
393
  app=app,
392
394
  base_url=base_url,
393
395
  filter_set=filter_set,
@@ -48,11 +48,20 @@ class OpenAPIParameter(Parameter):
48
48
 
49
49
  @property
50
50
  def is_header(self) -> bool:
51
- raise NotImplementedError
51
+ return self.location in ("header", "cookie")
52
52
 
53
53
  def as_json_schema(self, operation: APIOperation) -> dict[str, Any]:
54
54
  """Convert parameter's definition to JSON Schema."""
55
+ # JSON Schema allows `examples` as an array
56
+ if self.examples_field in self.definition:
57
+ examples = [
58
+ example["value"] for example in self.definition[self.examples_field].values() if "value" in example
59
+ ]
60
+ else:
61
+ examples = None
55
62
  schema = self.from_open_api_to_json_schema(operation, self.definition)
63
+ if examples is not None:
64
+ schema["examples"] = examples
56
65
  return self.transform_keywords(schema)
57
66
 
58
67
  def transform_keywords(self, schema: dict[str, Any]) -> dict[str, Any]:
@@ -117,12 +126,10 @@ class OpenAPI20Parameter(OpenAPIParameter):
117
126
  "uniqueItems",
118
127
  "enum",
119
128
  "multipleOf",
129
+ "example",
130
+ "examples",
120
131
  )
121
132
 
122
- @property
123
- def is_header(self) -> bool:
124
- return self.location == "header"
125
-
126
133
 
127
134
  @dataclass(eq=False)
128
135
  class OpenAPI30Parameter(OpenAPIParameter):
@@ -163,12 +170,10 @@ class OpenAPI30Parameter(OpenAPIParameter):
163
170
  "properties",
164
171
  "additionalProperties",
165
172
  "format",
173
+ "example",
174
+ "examples",
166
175
  )
167
176
 
168
- @property
169
- def is_header(self) -> bool:
170
- return self.location in ("header", "cookie")
171
-
172
177
  def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
173
178
  open_api_schema = get_parameter_schema(operation, open_api_schema)
174
179
  return super().from_open_api_to_json_schema(operation, open_api_schema)
@@ -217,6 +222,8 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
217
222
  "allOf",
218
223
  "properties",
219
224
  "additionalProperties",
225
+ "example",
226
+ "examples",
220
227
  )
221
228
  # NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
222
229
  # the precedence rules consistent.
@@ -48,6 +48,8 @@ class RunnerContext:
48
48
  current_response: GenericResponse | None = None
49
49
  # Total number of failures
50
50
  failures_count: int = 0
51
+ # The total number of completed test scenario
52
+ completed_scenarios: int = 0
51
53
  # Metrics collector for targeted testing
52
54
  metric_collector: TargetMetricCollector = field(default_factory=lambda: TargetMetricCollector(targets=[]))
53
55
 
@@ -64,6 +66,7 @@ class RunnerContext:
64
66
  return events.ScenarioStatus.REJECTED
65
67
 
66
68
  def reset_scenario(self) -> None:
69
+ self.completed_scenarios += 1
67
70
  self.current_step_status = None
68
71
  self.current_response = None
69
72
 
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Generator, Iterator, Type
9
9
  import hypothesis
10
10
  import requests
11
11
  from hypothesis.control import current_build_context
12
- from hypothesis.errors import Flaky
12
+ from hypothesis.errors import Flaky, Unsatisfiable
13
13
  from hypothesis.stateful import Rule
14
14
 
15
15
  from ..exceptions import CheckFailed
@@ -265,6 +265,13 @@ def _execute_state_machine_loop(
265
265
  ctx.mark_current_suite_as_seen_in_run()
266
266
  continue
267
267
  except Exception as exc:
268
+ if isinstance(exc, Unsatisfiable) and ctx.completed_scenarios > 0:
269
+ # Sometimes Hypothesis randomly gives up on generating some complex cases. However, if we know that
270
+ # values are possible to generate based on the previous observations, we retry the generation
271
+ if ctx.completed_scenarios >= config.hypothesis_settings.max_examples:
272
+ # Avoid infinite restarts
273
+ break
274
+ continue
268
275
  # Any other exception is an inner error and the test run should be stopped
269
276
  suite_status = events.SuiteStatus.ERROR
270
277
  event_queue.put(events.Errored(exception=exc))
schemathesis/types.py CHANGED
@@ -1,3 +1,4 @@
1
+ import enum
1
2
  from pathlib import Path
2
3
  from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
3
4
 
@@ -34,3 +35,10 @@ Hook = Union[
34
35
  RawAuth = Tuple[str, str]
35
36
  # Generic test with any arguments and no return
36
37
  GenericTest = Callable[..., None]
38
+
39
+
40
+ class Specification(str, enum.Enum):
41
+ """Specification of the given schema."""
42
+
43
+ OPENAPI = "openapi"
44
+ GRAPHQL = "graphql"