schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1760
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{runner → engine/phases}/probes.py +50 -67
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +139 -23
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +478 -369
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -58
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -790
  156. schemathesis/cli/output/short.py +0 -44
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1234
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -570
  184. schemathesis/runner/events.py +0 -329
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -1035
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -323
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -199
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.6.dist-info/METADATA +0 -356
  219. schemathesis-3.25.6.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,92 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Callable, Dict, List
4
-
5
- import hypothesis.strategies as st
6
- from requests.structures import CaseInsensitiveDict
7
-
8
- from ..links import OpenAPILink, get_all_links
9
- from ..utils import expand_status_code
10
-
11
- if TYPE_CHECKING:
12
- from ....stateful.state_machine import StepResult
13
- from ....models import APIOperation
14
-
15
- FilterFunction = Callable[["StepResult"], bool]
16
-
17
-
18
- @dataclass
19
- class Connection:
20
- source: str
21
- strategy: st.SearchStrategy[tuple[StepResult, OpenAPILink]]
22
-
23
-
24
- APIOperationConnections = Dict[str, List[Connection]]
25
-
26
-
27
- def apply(
28
- operation: APIOperation,
29
- bundles: dict[str, CaseInsensitiveDict],
30
- connections: APIOperationConnections,
31
- ) -> None:
32
- """Gather all connections based on Open API links definitions."""
33
- all_status_codes = list(operation.definition.resolved["responses"])
34
- for status_code, link in get_all_links(operation):
35
- target_operation = link.get_target_operation()
36
- strategy = bundles[operation.path][operation.method.upper()].filter(
37
- make_response_filter(status_code, all_status_codes)
38
- )
39
- connection = Connection(source=operation.verbose_name, strategy=_convert_strategy(strategy, link))
40
- connections[target_operation.verbose_name].append(connection)
41
-
42
-
43
- def _convert_strategy(
44
- strategy: st.SearchStrategy[StepResult], link: OpenAPILink
45
- ) -> st.SearchStrategy[tuple[StepResult, OpenAPILink]]:
46
- # This function is required to capture values properly (it won't work properly when lambda is defined in a loop)
47
- return strategy.map(lambda out: (out, link))
48
-
49
-
50
- def make_response_filter(status_code: str, all_status_codes: list[str]) -> FilterFunction:
51
- """Create a filter for stored responses.
52
-
53
- This filter will decide whether some response is suitable to use as a source for requesting some API operation.
54
- """
55
- if status_code == "default":
56
- return default_status_code(all_status_codes)
57
- return match_status_code(status_code)
58
-
59
-
60
- def match_status_code(status_code: str) -> FilterFunction:
61
- """Create a filter function that matches all responses with the given status code.
62
-
63
- Note that the status code can contain "X", which means any digit.
64
- For example, 50X will match all status codes from 500 to 509.
65
- """
66
- status_codes = set(expand_status_code(status_code))
67
-
68
- def compare(result: StepResult) -> bool:
69
- return result.response.status_code in status_codes
70
-
71
- # This name is displayed in the resulting strategy representation. For example, if you run your tests with
72
- # `--hypothesis-show-statistics`, then you can see `Bundle(name='GET /users/{user_id}').filter(match_200_response)`
73
- # which gives you information about the particularly used filter.
74
- compare.__name__ = f"match_{status_code}_response"
75
-
76
- return compare
77
-
78
-
79
- def default_status_code(status_codes: list[str]) -> FilterFunction:
80
- """Create a filter that matches all "default" responses.
81
-
82
- In Open API, the "default" response is the one that is used if no other options were matched.
83
- Therefore we need to match only responses that were not matched by other listed status codes.
84
- """
85
- expanded_status_codes = {
86
- status_code for value in status_codes if value != "default" for status_code in expand_status_code(value)
87
- }
88
-
89
- def match_default_response(result: StepResult) -> bool:
90
- return result.response.status_code not in expanded_status_codes
91
-
92
- return match_default_response
@@ -1,25 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Any
3
-
4
- from ...constants import HTTP_METHODS
5
-
6
-
7
- def is_pattern_error(exception: TypeError) -> bool:
8
- """Detect whether the input exception was caused by invalid type passed to `re.search`."""
9
- # This is intentionally simplistic and do not involve any traceback analysis
10
- return "expected string or bytes-like object" in str(exception)
11
-
12
-
13
- def find_numeric_http_status_codes(schema: Any) -> list[tuple[int, list[str | int]]]:
14
- if not isinstance(schema, dict):
15
- return []
16
- found = []
17
- for path, methods in schema.get("paths", {}).items():
18
- if isinstance(methods, dict):
19
- for method, definition in methods.items():
20
- if method not in HTTP_METHODS or not isinstance(definition, dict):
21
- continue
22
- for key in definition.get("responses", {}):
23
- if isinstance(key, int):
24
- found.append((key, [path, method]))
25
- return found
@@ -1,133 +0,0 @@
1
- from __future__ import annotations
2
- import enum
3
- import json
4
- from dataclasses import dataclass, field
5
- from typing import TYPE_CHECKING, Any, Callable, Generator
6
-
7
- from .. import GenerationConfig
8
- from ..exceptions import OperationSchemaError
9
- from ..models import APIOperation, Case
10
- from ..constants import NOT_SET
11
- from ..internal.result import Ok, Result
12
-
13
- if TYPE_CHECKING:
14
- import hypothesis
15
- from ..transports.responses import GenericResponse
16
- from .state_machine import APIStateMachine
17
-
18
-
19
- @enum.unique
20
- class Stateful(enum.Enum):
21
- none = 1
22
- links = 2
23
-
24
-
25
- @dataclass
26
- class ParsedData:
27
- """A structure that holds information parsed from a test outcome.
28
-
29
- It is used later to create a new version of an API operation that will reuse this data.
30
- """
31
-
32
- parameters: dict[str, Any]
33
- body: Any = NOT_SET
34
-
35
- def __hash__(self) -> int:
36
- """Custom hash simplifies deduplication of parsed data."""
37
- value = hash(tuple(self.parameters.items())) # parameters never contain nested dicts / lists
38
- if self.body is not NOT_SET:
39
- if isinstance(self.body, (dict, list)):
40
- # The simplest way to get a hash of a potentially nested structure
41
- value ^= hash(json.dumps(self.body, sort_keys=True))
42
- else:
43
- # These types should be hashable
44
- value ^= hash(self.body)
45
- return value
46
-
47
-
48
- @dataclass
49
- class StatefulTest:
50
- """A template for a test that will be executed after another one by reusing the outcomes from it."""
51
-
52
- name: str
53
-
54
- def parse(self, case: Case, response: GenericResponse) -> ParsedData:
55
- raise NotImplementedError
56
-
57
- def make_operation(self, collected: list[ParsedData]) -> APIOperation:
58
- raise NotImplementedError
59
-
60
-
61
- @dataclass
62
- class StatefulData:
63
- """Storage for data that will be used in later tests."""
64
-
65
- stateful_test: StatefulTest
66
- container: list[ParsedData] = field(default_factory=list)
67
-
68
- def make_operation(self) -> APIOperation:
69
- return self.stateful_test.make_operation(self.container)
70
-
71
- def store(self, case: Case, response: GenericResponse) -> None:
72
- """Parse and store data for a stateful test."""
73
- parsed = self.stateful_test.parse(case, response)
74
- self.container.append(parsed)
75
-
76
-
77
- @dataclass
78
- class Feedback:
79
- """Handler for feedback from tests.
80
-
81
- Provides a way to control runner's behavior from tests.
82
- """
83
-
84
- stateful: Stateful | None
85
- operation: APIOperation = field(repr=False)
86
- stateful_tests: dict[str, StatefulData] = field(default_factory=dict, repr=False)
87
-
88
- def add_test_case(self, case: Case, response: GenericResponse) -> None:
89
- """Store test data to reuse it in the future additional tests."""
90
- for stateful_test in case.operation.get_stateful_tests(response, self.stateful):
91
- data = self.stateful_tests.setdefault(stateful_test.name, StatefulData(stateful_test))
92
- data.store(case, response)
93
-
94
- def get_stateful_tests(
95
- self,
96
- test: Callable,
97
- settings: hypothesis.settings | None,
98
- generation_config: GenerationConfig | None,
99
- seed: int | None,
100
- as_strategy_kwargs: dict[str, Any] | Callable[[APIOperation], dict[str, Any]] | None,
101
- ) -> Generator[Result[tuple[APIOperation, Callable], OperationSchemaError], None, None]:
102
- """Generate additional tests that use data from the previous ones."""
103
- from .._hypothesis import create_test
104
-
105
- for data in self.stateful_tests.values():
106
- operation = data.make_operation()
107
- _as_strategy_kwargs: dict[str, Any] | None
108
- if callable(as_strategy_kwargs):
109
- _as_strategy_kwargs = as_strategy_kwargs(operation)
110
- else:
111
- _as_strategy_kwargs = as_strategy_kwargs
112
- test_function = create_test(
113
- operation=operation,
114
- test=test,
115
- settings=settings,
116
- seed=seed,
117
- data_generation_methods=operation.schema.data_generation_methods,
118
- generation_config=generation_config,
119
- as_strategy_kwargs=_as_strategy_kwargs,
120
- )
121
- yield Ok((operation, test_function))
122
-
123
-
124
- def run_state_machine_as_test(
125
- state_machine_factory: type[APIStateMachine], *, settings: hypothesis.settings | None = None
126
- ) -> None:
127
- """Run a state machine as a test.
128
-
129
- It automatically adds the `_min_steps` argument if ``Hypothesis`` is recent enough.
130
- """
131
- from hypothesis.stateful import run_state_machine_as_test as _run_state_machine_as_test
132
-
133
- return _run_state_machine_as_test(state_machine_factory, settings=settings, _min_steps=2)
schemathesis/targets.py DELETED
@@ -1,45 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
- from typing import TYPE_CHECKING, Callable
4
-
5
- if TYPE_CHECKING:
6
- from .models import Case
7
- from .transports.responses import GenericResponse
8
-
9
-
10
- @dataclass
11
- class TargetContext:
12
- """Context for targeted testing.
13
-
14
- :ivar Case case: Generated example that is being processed.
15
- :ivar GenericResponse response: API response.
16
- :ivar float response_time: API response time.
17
- """
18
-
19
- case: Case
20
- response: GenericResponse
21
- response_time: float
22
-
23
-
24
- def response_time(context: TargetContext) -> float:
25
- return context.response_time
26
-
27
-
28
- Target = Callable[[TargetContext], float]
29
- DEFAULT_TARGETS = ()
30
- OPTIONAL_TARGETS = (response_time,)
31
- ALL_TARGETS: tuple[Target, ...] = DEFAULT_TARGETS + OPTIONAL_TARGETS
32
-
33
-
34
- def register(target: Target) -> Target:
35
- """Register a new testing target for schemathesis CLI.
36
-
37
- :param target: A function that will be called to calculate a metric passed to ``hypothesis.target``.
38
- """
39
- from . import cli
40
-
41
- global ALL_TARGETS
42
-
43
- ALL_TARGETS += (target,)
44
- cli.TARGETS_TYPE.choices += (target.__name__,) # type: ignore
45
- return target
@@ -1,41 +0,0 @@
1
- from __future__ import annotations
2
- from typing import TYPE_CHECKING
3
-
4
- from .exceptions import UsageError
5
-
6
-
7
- if TYPE_CHECKING:
8
- from pyrate_limiter import Limiter
9
-
10
-
11
- def parse_units(rate: str) -> tuple[int, int]:
12
- from pyrate_limiter import Duration
13
-
14
- try:
15
- limit, interval_text = rate.split("/")
16
- interval = {
17
- "s": Duration.SECOND,
18
- "m": Duration.MINUTE,
19
- "h": Duration.HOUR,
20
- "d": Duration.DAY,
21
- }.get(interval_text)
22
- if interval is None:
23
- raise invalid_rate(rate)
24
- return int(limit), interval
25
- except ValueError as exc:
26
- raise invalid_rate(rate) from exc
27
-
28
-
29
- def invalid_rate(value: str) -> UsageError:
30
- return UsageError(
31
- f"Invalid rate limit value: `{value}`. Should be in form `limit/interval`. "
32
- "Example: `10/m` for 10 requests per minute."
33
- )
34
-
35
-
36
- def build_limiter(rate: str) -> Limiter:
37
- from pyrate_limiter import Limiter, RequestRate
38
-
39
- limit, interval = parse_units(rate)
40
- rate = RequestRate(limit, interval)
41
- return Limiter(rate)
@@ -1,5 +0,0 @@
1
- import base64
2
-
3
-
4
- def serialize_payload(payload: bytes) -> str:
5
- return base64.b64encode(payload).decode()
@@ -1,15 +0,0 @@
1
- from __future__ import annotations
2
- from typing import TYPE_CHECKING
3
-
4
- from ..types import RawAuth
5
-
6
- if TYPE_CHECKING:
7
- from requests.auth import HTTPDigestAuth
8
-
9
-
10
- def get_requests_auth(auth: RawAuth | None, auth_type: str | None) -> HTTPDigestAuth | RawAuth | None:
11
- from requests.auth import HTTPDigestAuth
12
-
13
- if auth and auth_type == "digest":
14
- return HTTPDigestAuth(*auth)
15
- return auth
@@ -1,35 +0,0 @@
1
- from __future__ import annotations
2
- import re
3
- from typing import Any
4
-
5
- from ..constants import USER_AGENT
6
-
7
-
8
- def setup_default_headers(kwargs: dict[str, Any]) -> None:
9
- headers = kwargs.setdefault("headers", {})
10
- if "user-agent" not in {header.lower() for header in headers}:
11
- kwargs["headers"]["User-Agent"] = USER_AGENT
12
-
13
-
14
- def is_latin_1_encodable(value: str) -> bool:
15
- """Header values are encoded to latin-1 before sending."""
16
- try:
17
- value.encode("latin-1")
18
- return True
19
- except UnicodeEncodeError:
20
- return False
21
-
22
-
23
- # Adapted from http.client._is_illegal_header_value
24
- INVALID_HEADER_RE = re.compile(r"\n(?![ \t])|\r(?![ \t\n])")
25
-
26
-
27
- def has_invalid_characters(name: str, value: str) -> bool:
28
- from requests.utils import check_header_validity
29
- from requests.exceptions import InvalidHeader
30
-
31
- try:
32
- check_header_validity((name, value))
33
- return bool(INVALID_HEADER_RE.search(value))
34
- except InvalidHeader:
35
- return True
@@ -1,52 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- import json
5
- from typing import Union, TYPE_CHECKING, NoReturn, Any
6
- from .._compat import JSONMixin
7
- from werkzeug.wrappers import Response as BaseResponse
8
-
9
- if TYPE_CHECKING:
10
- from httpx import Response as httpxResponse
11
- from requests import Response as requestsResponse
12
- from requests import PreparedRequest
13
-
14
-
15
- class WSGIResponse(BaseResponse, JSONMixin):
16
- # We store "requests" request to build a reproduction code
17
- request: PreparedRequest
18
-
19
- def on_json_loading_failed(self, e: json.JSONDecodeError) -> NoReturn:
20
- # We don't need a werkzeug-specific exception when JSON parsing error happens
21
- raise e
22
-
23
-
24
- def get_payload(response: GenericResponse) -> str:
25
- from httpx import Response as httpxResponse
26
- from requests import Response as requestsResponse
27
-
28
- if isinstance(response, (httpxResponse, requestsResponse)):
29
- return response.text
30
- return response.get_data(as_text=True)
31
-
32
-
33
- def get_json(response: GenericResponse) -> Any:
34
- from httpx import Response as httpxResponse
35
- from requests import Response as requestsResponse
36
-
37
- if isinstance(response, (httpxResponse, requestsResponse)):
38
- return json.loads(response.text)
39
- return response.json
40
-
41
-
42
- def get_reason(status_code: int) -> str:
43
- if sys.version_info < (3, 9) and status_code == 418:
44
- # Python 3.8 does not have 418 status in the `HTTPStatus` enum
45
- return "I'm a Teapot"
46
-
47
- import http.client
48
-
49
- return http.client.responses.get(status_code, "Unknown")
50
-
51
-
52
- GenericResponse = Union["httpxResponse", "requestsResponse", WSGIResponse]
schemathesis/types.py DELETED
@@ -1,35 +0,0 @@
1
- from pathlib import Path
2
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Set, Tuple, Union
3
-
4
- if TYPE_CHECKING:
5
- from hypothesis.strategies import SearchStrategy
6
- from .hooks import HookContext
7
-
8
- PathLike = Union[Path, str]
9
-
10
- Query = Dict[str, Any]
11
- # Body can be of any Python type that corresponds to JSON Schema types + `bytes`
12
- Body = Union[List, Dict[str, Any], str, int, float, bool, bytes]
13
- PathParameters = Dict[str, Any]
14
- Headers = Dict[str, Any]
15
- Cookies = Dict[str, Any]
16
- FormData = Dict[str, Any]
17
-
18
-
19
- class NotSet:
20
- pass
21
-
22
-
23
- RequestCert = Union[str, Tuple[str, str]]
24
-
25
-
26
- # A filter for path / method
27
- Filter = Union[str, List[str], Tuple[str], Set[str], NotSet]
28
-
29
- Hook = Union[
30
- Callable[["SearchStrategy"], "SearchStrategy"], Callable[["SearchStrategy", "HookContext"], "SearchStrategy"]
31
- ]
32
-
33
- RawAuth = Tuple[str, str]
34
- # Generic test with any arguments and no return
35
- GenericTest = Callable[..., None]
schemathesis/utils.py DELETED
@@ -1,169 +0,0 @@
1
- from __future__ import annotations
2
- import functools
3
- import operator
4
- from contextlib import contextmanager
5
- from inspect import getfullargspec
6
- from pathlib import Path
7
- from typing import (
8
- Any,
9
- Callable,
10
- Generator,
11
- NoReturn,
12
- Union,
13
- )
14
-
15
- import pytest
16
- from hypothesis import strategies as st
17
- from hypothesis.core import is_invalid_test
18
- from hypothesis.reporting import with_reporter
19
- from hypothesis.strategies import SearchStrategy
20
-
21
- from ._compat import InferType, get_signature
22
-
23
- # Backward-compat
24
- from .constants import NOT_SET # noqa: F401
25
- from .exceptions import SkipTest, UsageError
26
- from .types import GenericTest, PathLike
27
-
28
-
29
- def is_schemathesis_test(func: Callable) -> bool:
30
- """Check whether test is parametrized with schemathesis."""
31
- try:
32
- from .schemas import BaseSchema
33
-
34
- item = getattr(func, PARAMETRIZE_MARKER, None)
35
- # Comparison is needed to avoid false-positives when mocks are collected by pytest
36
- return isinstance(item, BaseSchema)
37
- except Exception:
38
- return False
39
-
40
-
41
- def fail_on_no_matches(node_id: str) -> NoReturn: # type: ignore
42
- pytest.fail(f"Test function {node_id} does not match any API operations and therefore has no effect")
43
-
44
-
45
- IGNORED_PATTERNS = (
46
- "Falsifying example: ",
47
- "Falsifying explicit example: ",
48
- "You can add @seed",
49
- "Failed to reproduce exception. Expected:",
50
- "Flaky example!",
51
- "Traceback (most recent call last):",
52
- "You can reproduce this example by temporarily",
53
- "Unreliable test timings",
54
- )
55
-
56
-
57
- @contextmanager
58
- def capture_hypothesis_output() -> Generator[list[str], None, None]:
59
- """Capture all output of Hypothesis into a list of strings.
60
-
61
- It allows us to have more granular control over Schemathesis output.
62
-
63
- Usage::
64
-
65
- @given(i=st.integers())
66
- def test(i):
67
- assert 0
68
-
69
- with capture_hypothesis_output() as output:
70
- test() # hypothesis test
71
- # output == ["Falsifying example: test(i=0)"]
72
- """
73
- output = []
74
-
75
- def get_output(value: str) -> None:
76
- # Drop messages that could be confusing in the Schemathesis context
77
- if value.startswith(IGNORED_PATTERNS):
78
- return
79
- output.append(value)
80
-
81
- # the following context manager is untyped
82
- with with_reporter(get_output): # type: ignore
83
- yield output
84
-
85
-
86
- GivenInput = Union[SearchStrategy, InferType]
87
- PARAMETRIZE_MARKER = "_schemathesis_test"
88
- GIVEN_ARGS_MARKER = "_schemathesis_given_args"
89
- GIVEN_KWARGS_MARKER = "_schemathesis_given_kwargs"
90
-
91
-
92
- def get_given_args(func: GenericTest) -> tuple:
93
- return getattr(func, GIVEN_ARGS_MARKER, ())
94
-
95
-
96
- def get_given_kwargs(func: GenericTest) -> dict[str, Any]:
97
- return getattr(func, GIVEN_KWARGS_MARKER, {})
98
-
99
-
100
- def is_given_applied(func: GenericTest) -> bool:
101
- return hasattr(func, GIVEN_ARGS_MARKER) or hasattr(func, GIVEN_KWARGS_MARKER)
102
-
103
-
104
- def given_proxy(*args: GivenInput, **kwargs: GivenInput) -> Callable[[GenericTest], GenericTest]:
105
- """Proxy Hypothesis strategies to ``hypothesis.given``."""
106
-
107
- def wrapper(func: GenericTest) -> GenericTest:
108
- if hasattr(func, GIVEN_ARGS_MARKER):
109
-
110
- def wrapped_test(*_: Any, **__: Any) -> NoReturn:
111
- raise UsageError(
112
- f"You have applied `given` to the `{func.__name__}` test more than once, which "
113
- "overrides the previous decorator. You need to pass all arguments to the same `given` call."
114
- )
115
-
116
- return wrapped_test
117
-
118
- setattr(func, GIVEN_ARGS_MARKER, args)
119
- setattr(func, GIVEN_KWARGS_MARKER, kwargs)
120
- return func
121
-
122
- return wrapper
123
-
124
-
125
- def merge_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> dict[str, Any]:
126
- """Merge positional arguments to ``@schema.given`` into a dictionary with keyword arguments.
127
-
128
- Kwargs are modified inplace.
129
- """
130
- if args:
131
- argspec = getfullargspec(func)
132
- for name, strategy in zip(reversed([arg for arg in argspec.args if arg != "case"]), reversed(args)):
133
- kwargs[name] = strategy
134
- return kwargs
135
-
136
-
137
- def validate_given_args(func: GenericTest, args: tuple, kwargs: dict[str, Any]) -> Callable | None:
138
- signature = get_signature(func)
139
- return is_invalid_test(func, signature, args, kwargs) # type: ignore
140
-
141
-
142
- def compose(*functions: Callable) -> Callable:
143
- """Compose multiple functions into a single one."""
144
-
145
- def noop(x: Any) -> Any:
146
- return x
147
-
148
- return functools.reduce(lambda f, g: lambda x: f(g(x)), functions, noop)
149
-
150
-
151
- def combine_strategies(strategies: list[st.SearchStrategy]) -> st.SearchStrategy:
152
- """Combine a list of strategies into a single one.
153
-
154
- If the input is `[a, b, c]`, then the result is equivalent to `a | b | c`.
155
- """
156
- return functools.reduce(operator.or_, strategies[1:], strategies[0])
157
-
158
-
159
- def skip(operation_name: str) -> NoReturn:
160
- raise SkipTest(f"It is not possible to generate negative test cases for `{operation_name}`")
161
-
162
-
163
- def _ensure_parent(path: PathLike, fail_silently: bool = True) -> None:
164
- # Try to create the parent dir
165
- try:
166
- Path(path).parent.mkdir(mode=0o755, parents=True, exist_ok=True)
167
- except OSError:
168
- if not fail_silently:
169
- raise