schemathesis 3.19.7__py3-none-any.whl → 3.20.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.
Files changed (46) hide show
  1. schemathesis/_compat.py +3 -2
  2. schemathesis/_hypothesis.py +21 -6
  3. schemathesis/_xml.py +177 -0
  4. schemathesis/auths.py +48 -10
  5. schemathesis/cli/__init__.py +77 -19
  6. schemathesis/cli/callbacks.py +42 -18
  7. schemathesis/cli/context.py +2 -1
  8. schemathesis/cli/output/default.py +102 -34
  9. schemathesis/cli/sanitization.py +15 -0
  10. schemathesis/code_samples.py +141 -0
  11. schemathesis/constants.py +1 -24
  12. schemathesis/exceptions.py +127 -26
  13. schemathesis/experimental/__init__.py +85 -0
  14. schemathesis/extra/pytest_plugin.py +10 -4
  15. schemathesis/fixups/__init__.py +8 -2
  16. schemathesis/fixups/fast_api.py +11 -1
  17. schemathesis/fixups/utf8_bom.py +7 -1
  18. schemathesis/hooks.py +63 -0
  19. schemathesis/lazy.py +10 -4
  20. schemathesis/loaders.py +57 -0
  21. schemathesis/models.py +120 -96
  22. schemathesis/parameters.py +3 -0
  23. schemathesis/runner/__init__.py +3 -0
  24. schemathesis/runner/events.py +55 -20
  25. schemathesis/runner/impl/core.py +54 -54
  26. schemathesis/runner/serialization.py +75 -34
  27. schemathesis/sanitization.py +248 -0
  28. schemathesis/schemas.py +21 -6
  29. schemathesis/serializers.py +32 -3
  30. schemathesis/service/serialization.py +5 -1
  31. schemathesis/specs/graphql/loaders.py +44 -13
  32. schemathesis/specs/graphql/schemas.py +56 -25
  33. schemathesis/specs/openapi/_hypothesis.py +11 -23
  34. schemathesis/specs/openapi/definitions.py +572 -0
  35. schemathesis/specs/openapi/loaders.py +100 -49
  36. schemathesis/specs/openapi/parameters.py +2 -2
  37. schemathesis/specs/openapi/schemas.py +87 -13
  38. schemathesis/specs/openapi/security.py +1 -0
  39. schemathesis/stateful.py +2 -2
  40. schemathesis/utils.py +30 -9
  41. schemathesis-3.20.1.dist-info/METADATA +342 -0
  42. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/RECORD +45 -39
  43. schemathesis-3.19.7.dist-info/METADATA +0 -291
  44. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/WHEEL +0 -0
  45. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/entry_points.txt +0 -0
  46. {schemathesis-3.19.7.dist-info → schemathesis-3.20.1.dist-info}/licenses/LICENSE +0 -0
@@ -10,17 +10,37 @@ import hypothesis
10
10
  from click.types import LazyFile # type: ignore
11
11
  from requests import PreparedRequest, RequestException
12
12
 
13
- from .. import exceptions, throttling, utils
14
- from ..constants import CodeSampleStyle, DataGenerationMethod
13
+ from .. import exceptions, experimental, throttling, utils
14
+ from ..code_samples import CodeSampleStyle
15
+ from ..constants import DataGenerationMethod
15
16
  from ..service.hosts import get_temporary_hosts_file
16
17
  from ..stateful import Stateful
17
18
  from ..types import PathLike
18
19
  from .constants import DEFAULT_WORKERS
19
20
 
21
+ INVALID_DERANDOMIZE_MESSAGE = (
22
+ "`--hypothesis-derandomize` implies no database, so passing `--hypothesis-database` too is invalid."
23
+ )
20
24
  MISSING_CASSETTE_PATH_ARGUMENT_MESSAGE = (
21
25
  'Missing argument, "--cassette-path" should be specified as well if you use "--cassette-preserve-exact-body-bytes".'
22
26
  )
23
27
  INVALID_SCHEMA_MESSAGE = "Invalid SCHEMA, must be a valid URL, file path or an API name from Schemathesis.io."
28
+ FILE_DOES_NOT_EXIST_MESSAGE = "The specified file does not exist. Please provide a valid path to an existing file."
29
+ INVALID_BASE_URL_MESSAGE = (
30
+ "The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
31
+ "Make sure it is a properly formatted URL."
32
+ )
33
+ MISSING_BASE_URL_MESSAGE = "The `--base-url` option is required when specifying a schema via a file."
34
+ APPLICATION_FORMAT_MESSAGE = """Unable to import application from the provided module.
35
+ The `--app` option should follow this format:
36
+
37
+ module_path:variable_name
38
+
39
+ - `module_path`: A path to an importable Python module.
40
+ - `variable_name`: The name of the application variable within that module.
41
+
42
+ Example: `st run --app=your_module:app ...`"""
43
+ MISSING_REQUEST_CERT_MESSAGE = "The `--request-cert` option must be specified if `--request-cert-key` is used."
24
44
 
25
45
 
26
46
  @enum.unique
@@ -47,7 +67,7 @@ def parse_schema_kind(schema: str, app: Optional[str]) -> SchemaInputKind:
47
67
  raise click.UsageError(INVALID_SCHEMA_MESSAGE)
48
68
  if netloc:
49
69
  return SchemaInputKind.URL
50
- if utils.file_exists(schema):
70
+ if utils.file_exists(schema) or utils.is_filename(schema):
51
71
  return SchemaInputKind.PATH
52
72
  if app is not None:
53
73
  return SchemaInputKind.APP_PATH
@@ -69,7 +89,11 @@ def validate_schema(
69
89
  if kind == SchemaInputKind.PATH:
70
90
  # Base URL is required if it is not a dry run
71
91
  if app is None and base_url is None and not dry_run:
72
- raise click.UsageError('Missing argument, "--base-url" is required for SCHEMA specified by file.')
92
+ if not utils.file_exists(schema):
93
+ message = FILE_DOES_NOT_EXIST_MESSAGE
94
+ else:
95
+ message = MISSING_BASE_URL_MESSAGE
96
+ raise click.UsageError(message)
73
97
  if kind == SchemaInputKind.NAME:
74
98
  if api_name is not None:
75
99
  raise click.UsageError(f"Got unexpected extra argument ({api_name})")
@@ -86,9 +110,9 @@ def validate_base_url(ctx: click.core.Context, param: click.core.Parameter, raw_
86
110
  try:
87
111
  netloc = urlparse(raw_value).netloc
88
112
  except ValueError as exc:
89
- raise click.UsageError("Invalid base URL") from exc
113
+ raise click.UsageError(INVALID_BASE_URL_MESSAGE) from exc
90
114
  if raw_value and not netloc:
91
- raise click.UsageError("Invalid base URL")
115
+ raise click.UsageError(INVALID_BASE_URL_MESSAGE)
92
116
  return raw_value
93
117
 
94
118
 
@@ -104,14 +128,6 @@ def validate_rate_limit(
104
128
  raise click.UsageError(exc.args[0]) from exc
105
129
 
106
130
 
107
- APPLICATION_FORMAT_MESSAGE = (
108
- "Can not import application from the given module!\n"
109
- "The `--app` option value should be in format:\n\n path:variable\n\n"
110
- "where `path` is an importable path to a Python module,\n"
111
- "and `variable` is a variable name inside that module."
112
- )
113
-
114
-
115
131
  def validate_app(ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]) -> Optional[str]:
116
132
  if raw_value is None:
117
133
  return raw_value
@@ -139,9 +155,7 @@ def validate_hypothesis_database(
139
155
  if raw_value is None:
140
156
  return raw_value
141
157
  if ctx.params.get("hypothesis_derandomize"):
142
- raise click.UsageError(
143
- "--hypothesis-derandomize implies no database, so passing --hypothesis-database too is invalid."
144
- )
158
+ raise click.UsageError(INVALID_DERANDOMIZE_MESSAGE)
145
159
  return raw_value
146
160
 
147
161
 
@@ -195,7 +209,7 @@ def validate_request_cert_key(
195
209
  ctx: click.core.Context, param: click.core.Parameter, raw_value: Optional[str]
196
210
  ) -> Optional[str]:
197
211
  if raw_value is not None and "request_cert" not in ctx.params:
198
- raise click.UsageError('Missing argument, "--request-cert" should be specified as well.')
212
+ raise click.UsageError(MISSING_REQUEST_CERT_MESSAGE)
199
213
  return raw_value
200
214
 
201
215
 
@@ -219,6 +233,16 @@ def convert_stateful(ctx: click.core.Context, param: click.core.Parameter, value
219
233
  return Stateful[value]
220
234
 
221
235
 
236
+ def convert_experimental(
237
+ ctx: click.core.Context, param: click.core.Parameter, value: Tuple[str, ...]
238
+ ) -> List[experimental.Experiment]:
239
+ return [
240
+ feature
241
+ for feature in experimental.GLOBAL_EXPERIMENTS.available
242
+ if feature.name in value or os.getenv(feature.env_var, "").lower() in TRUE_VALUES
243
+ ]
244
+
245
+
222
246
  def convert_checks(ctx: click.core.Context, param: click.core.Parameter, value: Tuple[List[str]]) -> List[str]:
223
247
  return sum(value, [])
224
248
 
@@ -6,7 +6,7 @@ from typing import List, Optional, Union
6
6
 
7
7
  import hypothesis
8
8
 
9
- from ..constants import CodeSampleStyle
9
+ from ..code_samples import CodeSampleStyle
10
10
  from ..runner.serialization import SerializedTestResult
11
11
 
12
12
 
@@ -31,6 +31,7 @@ class ExecutionContext:
31
31
  workers_num: int = 1
32
32
  rate_limit: Optional[str] = None
33
33
  show_errors_tracebacks: bool = False
34
+ wait_for_schema: Optional[float] = None
34
35
  validate_schema: bool = True
35
36
  operations_processed: int = 0
36
37
  # It is set in runtime, from a `Initialized` event
@@ -11,18 +11,22 @@ import requests
11
11
 
12
12
  from ... import service
13
13
  from ..._compat import metadata
14
- from ...constants import DISCORD_LINK, FLAKY_FAILURE_MESSAGE, CodeSampleStyle, __version__
14
+ from ...constants import (
15
+ DISCORD_LINK,
16
+ FLAKY_FAILURE_MESSAGE,
17
+ REPORT_SUGGESTION_ENV_VAR,
18
+ SCHEMATHESIS_TEST_CASE_HEADER,
19
+ __version__,
20
+ )
21
+ from ...experimental import GLOBAL_EXPERIMENTS
15
22
  from ...models import Response, Status
16
23
  from ...runner import events
24
+ from ...runner.events import InternalErrorType, SchemaErrorType
17
25
  from ...runner.serialization import SerializedCase, SerializedError, SerializedTestResult, deduplicate_failures
26
+ from ..callbacks import FALSE_VALUES
18
27
  from ..context import ExecutionContext, FileReportContext, ServiceReportContext
19
28
  from ..handlers import EventHandler
20
29
 
21
- DISABLE_SCHEMA_VALIDATION_MESSAGE = (
22
- "\nYou can disable input schema validation with --validate-schema=false "
23
- "command-line option\nIn this case, Schemathesis cannot guarantee proper"
24
- " behavior during the test run"
25
- )
26
30
  ISSUE_TRACKER_URL = (
27
31
  "https://github.com/schemathesis/schemathesis/issues/new?"
28
32
  "labels=Status%3A+Review+Needed%2C+Type%3A+Bug&template=bug_report.md&title=%5BBUG%5D"
@@ -140,8 +144,7 @@ def display_errors(context: ExecutionContext, event: events.Finished) -> None:
140
144
  "Add this option to your command line parameters to see full tracebacks: --show-errors-tracebacks", fg="red"
141
145
  )
142
146
  click.secho(
143
- f"\nIf you need assistance in solving this, feel free to join our Discord server and report it:\n\n"
144
- f" {DISCORD_LINK}\n",
147
+ f"\nNeed more help?\n" f" Join our Discord server: {DISCORD_LINK}",
145
148
  fg="red",
146
149
  )
147
150
 
@@ -150,7 +153,7 @@ def display_single_error(context: ExecutionContext, result: SerializedTestResult
150
153
  display_subsection(result)
151
154
  should_display_full_traceback_message = False
152
155
  for error in result.errors:
153
- should_display_full_traceback_message |= _display_error(context, error, result.seed)
156
+ should_display_full_traceback_message |= _display_error(context, error)
154
157
  return should_display_full_traceback_message
155
158
 
156
159
 
@@ -162,24 +165,20 @@ def display_generic_errors(context: ExecutionContext, errors: List[SerializedErr
162
165
 
163
166
  def display_full_traceback_message(exception: str) -> bool:
164
167
  # Some errors should not trigger the message that suggests to show full tracebacks to the user
165
- return not exception.startswith("DeadlineExceeded")
168
+ return not exception.startswith(("DeadlineExceeded", "OperationSchemaError"))
166
169
 
167
170
 
168
- def _display_error(context: ExecutionContext, error: SerializedError, seed: Optional[int] = None) -> bool:
171
+ def _display_error(context: ExecutionContext, error: SerializedError) -> bool:
169
172
  if context.show_errors_tracebacks:
170
173
  message = error.exception_with_traceback
171
174
  else:
172
175
  message = error.exception
173
- if error.exception.startswith("InvalidSchema") and context.validate_schema:
174
- message += DISABLE_SCHEMA_VALIDATION_MESSAGE + "\n"
175
176
  if error.exception.startswith("DeadlineExceeded"):
176
177
  message += (
177
178
  "Consider extending the deadline with the `--hypothesis-deadline` CLI option.\n"
178
179
  "You can disable it completely with `--hypothesis-deadline=None`.\n"
179
180
  )
180
181
  click.secho(message, fg="red")
181
- if error.example is not None:
182
- display_example(context, error.example, seed=seed)
183
182
  return display_full_traceback_message(error.exception)
184
183
 
185
184
 
@@ -239,10 +238,20 @@ def display_example(
239
238
  if response is not None and response.body is not None:
240
239
  payload = base64.b64decode(response.body).decode(response.encoding or "utf8", errors="replace")
241
240
  click.secho(f"Response status: {response.status_code}\nResponse payload: `{payload}`\n", fg="red")
242
- if context.code_sample_style == CodeSampleStyle.python:
243
- click.secho(f"Run this Python code to reproduce this failure: \n\n {case.requests_code}\n", fg="red")
244
- if context.code_sample_style == CodeSampleStyle.curl:
245
- click.secho(f"Run this cURL command to reproduce this failure: \n\n {case.curl_code}\n", fg="red")
241
+ request_body = base64.b64decode(case.body).decode() if case.body is not None else None
242
+ code_sample = context.code_sample_style.generate(
243
+ method=case.method,
244
+ url=case.url,
245
+ body=request_body,
246
+ headers=case.headers,
247
+ verify=case.verify,
248
+ extra_headers=case.extra_headers,
249
+ )
250
+ click.secho(
251
+ f"Run this {context.code_sample_style.verbose_name} to reproduce this failure: \n\n {code_sample}\n",
252
+ fg="red",
253
+ )
254
+ click.secho(f"{SCHEMATHESIS_TEST_CASE_HEADER}: {case.id}\n", fg="red")
246
255
  if seed is not None:
247
256
  click.secho(f"Or add this option to your command line parameters: --hypothesis-seed={seed}", fg="red")
248
257
 
@@ -295,6 +304,22 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
295
304
  for warning in event.warnings:
296
305
  click.secho(f" - {warning}", fg="yellow")
297
306
 
307
+ if len(GLOBAL_EXPERIMENTS.enabled) > 0:
308
+ click.secho("\nExperimental Features:", bold=True)
309
+ for experiment in sorted(GLOBAL_EXPERIMENTS.enabled, key=lambda e: e.name):
310
+ click.secho(f" - {experiment.verbose_name}: {experiment.description}")
311
+ click.secho(f" Feedback: {experiment.discussion_url}")
312
+ click.echo()
313
+ click.echo(
314
+ "Your feedback is crucial for experimental features. "
315
+ "Please visit the provided URL(s) to share your thoughts."
316
+ )
317
+
318
+ if event.failed_count > 0:
319
+ click.echo(
320
+ f"\n{bold('Note')}: The '{SCHEMATHESIS_TEST_CASE_HEADER}' header can be used to correlate test cases with server logs for debugging."
321
+ )
322
+
298
323
  if context.report is not None and not context.is_interrupted:
299
324
  if isinstance(context.report, FileReportContext):
300
325
  click.echo()
@@ -304,6 +329,9 @@ def display_statistic(context: ExecutionContext, event: events.Finished) -> None
304
329
  click.echo()
305
330
  handle_service_integration(context.report)
306
331
  else:
332
+ env_var = os.getenv(REPORT_SUGGESTION_ENV_VAR)
333
+ if env_var is not None and env_var.lower() in FALSE_VALUES:
334
+ return
307
335
  click.echo()
308
336
  category = click.style("Hint", bold=True)
309
337
  click.echo(
@@ -354,7 +382,8 @@ def display_report_metadata(meta: service.Metadata) -> None:
354
382
  def display_service_error(event: service.Error) -> None:
355
383
  """Show information about an error during communication with Schemathesis.io."""
356
384
  if isinstance(event.exception, requests.HTTPError):
357
- status_code = event.exception.response.status_code
385
+ response = cast(requests.Response, event.exception.response)
386
+ status_code = response.status_code
358
387
  click.secho(f"Schemathesis.io responded with HTTP {status_code}", fg="red")
359
388
  if 500 <= status_code <= 599:
360
389
  # Server error, should be resolved soon
@@ -447,23 +476,62 @@ def display_check_result(check_name: str, results: Dict[Union[str, Status], int]
447
476
  click.echo(template.format(check_name, f"{success} / {total} passed", click.style(verdict, fg=color, bold=True)))
448
477
 
449
478
 
479
+ VERIFY_URL_SUGGESTION = "Verify that the URL points directly to the Open API schema"
480
+
481
+
482
+ def bold(option: str) -> str:
483
+ return click.style(option, bold=True)
484
+
485
+
486
+ SCHEMA_ERROR_SUGGESTIONS = {
487
+ # SSL-specific connection issue
488
+ SchemaErrorType.CONNECTION_SSL: f"Bypass SSL verification with {bold('`--request-tls-verify=false`')}.",
489
+ # Other connection problems
490
+ SchemaErrorType.CONNECTION_OTHER: f"Use {bold('`--wait-for-schema=NUM`')} to wait up to NUM seconds for schema availability.",
491
+ # Response issues
492
+ SchemaErrorType.UNEXPECTED_CONTENT_TYPE: VERIFY_URL_SUGGESTION,
493
+ SchemaErrorType.HTTP_FORBIDDEN: "Verify your API keys or authentication headers.",
494
+ SchemaErrorType.HTTP_NOT_FOUND: VERIFY_URL_SUGGESTION,
495
+ # OpenAPI specification issues
496
+ SchemaErrorType.OPEN_API_UNSPECIFIED_VERSION: f"Include the version in the schema or manually set it with {bold('`--force-schema-version`')}.",
497
+ SchemaErrorType.OPEN_API_UNSUPPORTED_VERSION: f"Proceed with {bold('`--force-schema-version`')}. Caution: May not be fully supported.",
498
+ SchemaErrorType.OPEN_API_INVALID_SCHEMA: f"Bypass validation using {bold('`--validate-schema=false`')}. Caution: May cause unexpected errors.",
499
+ # YAML specific issues
500
+ SchemaErrorType.YAML_NUMERIC_STATUS_CODES: "Convert numeric status codes to strings.",
501
+ SchemaErrorType.YAML_NON_STRING_KEYS: "Convert non-string keys to strings.",
502
+ # Unclassified
503
+ SchemaErrorType.UNCLASSIFIED: f"If you suspect this is a Schemathesis issue and the schema is valid, please report it and include the schema if you can: {ISSUE_TRACKER_URL}",
504
+ }
505
+
506
+
507
+ def should_skip_suggestion(context: ExecutionContext, event: events.InternalError) -> bool:
508
+ return event.subtype == SchemaErrorType.CONNECTION_OTHER and context.wait_for_schema is not None
509
+
510
+
450
511
  def display_internal_error(context: ExecutionContext, event: events.InternalError) -> None:
451
- click.secho(event.message, fg="red")
512
+ click.secho(event.title, fg="red", bold=True)
452
513
  click.echo()
453
- if event.exception:
454
- if context.show_errors_tracebacks:
455
- message = (
456
- f"Error: {event.exception_with_traceback}\n"
457
- f"Please, consider reporting the traceback below it to our issue tracker: {ISSUE_TRACKER_URL}"
458
- )
514
+ click.secho(event.message)
515
+ if event.type == InternalErrorType.SCHEMA:
516
+ extras = event.extras
517
+ elif context.show_errors_tracebacks:
518
+ extras = [entry for entry in event.exception_with_traceback.splitlines() if entry]
519
+ else:
520
+ extras = [event.exception]
521
+ if extras:
522
+ click.echo()
523
+ for extra in extras:
524
+ click.secho(f" {extra}")
525
+ if not should_skip_suggestion(context, event):
526
+ if event.type == InternalErrorType.SCHEMA and isinstance(event.subtype, SchemaErrorType):
527
+ suggestion = SCHEMA_ERROR_SUGGESTIONS.get(event.subtype)
528
+ elif context.show_errors_tracebacks:
529
+ suggestion = f"Please consider reporting the traceback above to our issue tracker: {ISSUE_TRACKER_URL}."
459
530
  else:
460
- message = (
461
- f"Error: {event.exception}\n"
462
- f"Add this option to your command line parameters to see full tracebacks: --show-errors-tracebacks\n"
463
- )
464
- if event.exception_type == "schemathesis.exceptions.SchemaLoadingError":
465
- message += "\n" + DISABLE_SCHEMA_VALIDATION_MESSAGE
466
- click.secho(message, fg="red")
531
+ suggestion = f"To see full tracebacks, add {bold('`--show-errors-tracebacks`')} to your CLI options"
532
+ # Display suggestion if any
533
+ if suggestion is not None:
534
+ click.secho(f"\n{click.style('Tip:', bold=True, fg='green')} {suggestion}")
467
535
 
468
536
 
469
537
  def handle_initialized(context: ExecutionContext, event: events.Initialized) -> None:
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+
3
+ from ..runner import events
4
+ from ..sanitization import sanitize_serialized_check, sanitize_serialized_interaction
5
+ from .handlers import EventHandler, ExecutionContext
6
+
7
+
8
+ @dataclass
9
+ class SanitizationHandler(EventHandler):
10
+ def handle_event(self, context: ExecutionContext, event: events.ExecutionEvent) -> None:
11
+ if isinstance(event, events.AfterExecution):
12
+ for check in event.result.checks:
13
+ sanitize_serialized_check(check)
14
+ for interaction in event.result.interactions:
15
+ sanitize_serialized_interaction(interaction)
@@ -0,0 +1,141 @@
1
+ from enum import Enum
2
+ from shlex import quote
3
+ from typing import Optional, Union
4
+
5
+ from requests.structures import CaseInsensitiveDict
6
+ from requests.utils import default_headers
7
+
8
+ from .constants import SCHEMATHESIS_TEST_CASE_HEADER, DataGenerationMethod
9
+ from .types import Headers
10
+
11
+ DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
12
+ # These headers are added automatically by Schemathesis or `requests`.
13
+ # Do not show them in code samples to make them more readable
14
+ EXCLUDED_HEADERS = CaseInsensitiveDict(
15
+ {
16
+ "Content-Length": None,
17
+ "Transfer-Encoding": None,
18
+ SCHEMATHESIS_TEST_CASE_HEADER: None,
19
+ **default_headers(),
20
+ }
21
+ )
22
+
23
+
24
+ class CodeSampleStyle(str, Enum):
25
+ """Controls the style of code samples for failure reproduction."""
26
+
27
+ python = "python"
28
+ curl = "curl"
29
+
30
+ @property
31
+ def verbose_name(self) -> str:
32
+ return {
33
+ self.curl: "cURL command",
34
+ self.python: "Python code",
35
+ }[self]
36
+
37
+ @classmethod
38
+ def default(cls) -> "CodeSampleStyle":
39
+ return cls.curl
40
+
41
+ @classmethod
42
+ def from_str(cls, value: str) -> "CodeSampleStyle":
43
+ try:
44
+ return cls[value]
45
+ except KeyError:
46
+ available_styles = ", ".join(cls)
47
+ raise ValueError(
48
+ f"Invalid value for code sample style: {value}. Available styles: {available_styles}"
49
+ ) from None
50
+
51
+ def generate(
52
+ self,
53
+ *,
54
+ method: str,
55
+ url: str,
56
+ body: Optional[Union[str, bytes]],
57
+ headers: Optional[Headers],
58
+ verify: bool,
59
+ extra_headers: Optional[Headers] = None,
60
+ ) -> str:
61
+ """Generate a code snippet for making HTTP requests."""
62
+ handlers = {
63
+ self.curl: _generate_curl,
64
+ self.python: _generate_requests,
65
+ }
66
+ return handlers[self](
67
+ method=method, url=url, body=body, headers=_filter_headers(headers, extra_headers), verify=verify
68
+ )
69
+
70
+
71
+ def _filter_headers(headers: Optional[Headers], extra: Optional[Headers] = None) -> Headers:
72
+ headers = headers.copy() if headers else {}
73
+ if extra is not None:
74
+ for key, value in extra.items():
75
+ if key not in EXCLUDED_HEADERS:
76
+ headers[key] = value
77
+ return headers
78
+
79
+
80
+ def _generate_curl(
81
+ *,
82
+ method: str,
83
+ url: str,
84
+ body: Optional[Union[str, bytes]],
85
+ headers: Headers,
86
+ verify: bool,
87
+ ) -> str:
88
+ """Create a cURL command to reproduce an HTTP request."""
89
+ command = f"curl -X {method}"
90
+ for key, value in headers.items():
91
+ header = f"{key}: {value}"
92
+ command += f" -H {quote(header)}"
93
+ if body:
94
+ if isinstance(body, bytes):
95
+ body = body.decode("utf-8", errors="replace")
96
+ command += f" -d {quote(body)}"
97
+ if not verify:
98
+ command += " --insecure"
99
+ return f"{command} {quote(url)}"
100
+
101
+
102
+ def _generate_requests(
103
+ *,
104
+ method: str,
105
+ url: str,
106
+ body: Optional[Union[str, bytes]],
107
+ headers: Headers,
108
+ verify: bool,
109
+ ) -> str:
110
+ """Create a Python code to reproduce an HTTP request."""
111
+ url = _escape_single_quotes(url)
112
+ command = f"requests.{method.lower()}('{url}'"
113
+ if body:
114
+ command += f", data={repr(body)}"
115
+ if headers:
116
+ command += f", headers={repr(headers)}"
117
+ if not verify:
118
+ command += ", verify=False"
119
+ command += ")"
120
+ return command
121
+
122
+
123
+ def _escape_single_quotes(url: str) -> str:
124
+ """Escape single quotes in a string, so it is usable as in generated Python code.
125
+
126
+ The usual ``str.replace`` is not suitable as it may convert already escaped quotes to not-escaped.
127
+ """
128
+ result = []
129
+ escape = False
130
+ for char in url:
131
+ if escape:
132
+ result.append(char)
133
+ escape = False
134
+ elif char == "\\":
135
+ result.append(char)
136
+ escape = True
137
+ elif char == "'":
138
+ result.append("\\'")
139
+ else:
140
+ result.append(char)
141
+ return "".join(result)
schemathesis/constants.py CHANGED
@@ -34,9 +34,6 @@ SERIALIZERS_SUGGESTION_MESSAGE = (
34
34
  "and Schemathesis will be able to make API calls with this media type. \n"
35
35
  "See https://schemathesis.readthedocs.io/en/stable/how.html#payload-serialization for more information."
36
36
  )
37
- USE_WAIT_FOR_SCHEMA_SUGGESTION_MESSAGE = (
38
- "You can use `--wait-for-schema=NUM` to wait for a maximum of NUM seconds on the API schema availability."
39
- )
40
37
  FLAKY_FAILURE_MESSAGE = "[FLAKY] Schemathesis was not able to reliably reproduce this failure"
41
38
  BOM_MARK = "\ufeff"
42
39
  WAIT_FOR_SCHEMA_INTERVAL = 0.05
@@ -44,6 +41,7 @@ HOOKS_MODULE_ENV_VAR = "SCHEMATHESIS_HOOKS"
44
41
  API_NAME_ENV_VAR = "SCHEMATHESIS_API_NAME"
45
42
  BASE_URL_ENV_VAR = "SCHEMATHESIS_BASE_URL"
46
43
  WAIT_FOR_SCHEMA_ENV_VAR = "SCHEMATHESIS_WAIT_FOR_SCHEMA"
44
+ REPORT_SUGGESTION_ENV_VAR = "SCHEMATHESIS_REPORT_SUGGESTION"
47
45
 
48
46
 
49
47
  class DataGenerationMethod(str, Enum):
@@ -74,24 +72,3 @@ class DataGenerationMethod(str, Enum):
74
72
 
75
73
 
76
74
  DEFAULT_DATA_GENERATION_METHODS = (DataGenerationMethod.default(),)
77
-
78
-
79
- class CodeSampleStyle(str, Enum):
80
- """Controls the style of code samples for failure reproduction."""
81
-
82
- python = "python"
83
- curl = "curl"
84
-
85
- @classmethod
86
- def default(cls) -> "CodeSampleStyle":
87
- return cls.curl
88
-
89
- @classmethod
90
- def from_str(cls, value: str) -> "CodeSampleStyle":
91
- try:
92
- return cls[value]
93
- except KeyError:
94
- available_styles = ", ".join(cls)
95
- raise ValueError(
96
- f"Invalid value for code sample style: {value}. Available styles: {available_styles}"
97
- ) from None