schemathesis 3.39.15__py3-none-any.whl → 4.0.0__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 (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +238 -308
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -712
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.15.dist-info/METADATA +0 -293
  251. schemathesis-3.39.15.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -1,202 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import enum
4
- import os
5
- from dataclasses import asdict, dataclass
6
- from typing import Protocol, runtime_checkable
7
-
8
-
9
- @enum.unique
10
- class CIProvider(enum.Enum):
11
- """A set of supported CI providers."""
12
-
13
- GITHUB = "github"
14
- GITLAB = "gitlab"
15
-
16
-
17
- @runtime_checkable
18
- class Environment(Protocol):
19
- provider: CIProvider
20
- variable_name: str
21
- verbose_name: str
22
-
23
- @classmethod
24
- def is_set(cls) -> bool:
25
- pass
26
-
27
- @classmethod
28
- def from_env(cls) -> Environment:
29
- pass
30
-
31
- def asdict(self) -> dict[str, str | None]:
32
- pass
33
-
34
- def as_env(self) -> dict[str, str | None]:
35
- pass
36
-
37
-
38
- def environment() -> Environment | None:
39
- """Collect environment data for a supported CI provider."""
40
- provider = detect()
41
- if provider == CIProvider.GITHUB:
42
- return GitHubActionsEnvironment.from_env()
43
- if provider == CIProvider.GITLAB:
44
- return GitLabCIEnvironment.from_env()
45
- return None
46
-
47
-
48
- def detect() -> CIProvider | None:
49
- """Detect the current CI provider."""
50
- if GitHubActionsEnvironment.is_set():
51
- return GitHubActionsEnvironment.provider
52
- if GitLabCIEnvironment.is_set():
53
- return GitLabCIEnvironment.provider
54
- return None
55
-
56
-
57
- def _asdict(env: Environment) -> dict[str, str | None]:
58
- data = asdict(env) # type: ignore
59
- data["provider"] = env.provider.value
60
- return data
61
-
62
-
63
- @dataclass
64
- class GitHubActionsEnvironment:
65
- """Useful data to capture from GitHub Actions environment."""
66
-
67
- provider = CIProvider.GITHUB
68
- variable_name = "GITHUB_ACTIONS"
69
- verbose_name = "GitHub Actions"
70
- asdict = _asdict
71
-
72
- # GitHub API URL.
73
- # For example, `https://api.github.com`
74
- api_url: str
75
- # The owner and repository name.
76
- # For example, `schemathesis/schemathesis`.
77
- repository: str
78
- # The name of the person or app that initiated the workflow.
79
- # For example, `Stranger6667`
80
- actor: str
81
- # The commit SHA that triggered the workflow.
82
- # For example, `e56e13224f08469841e106449f6467b769e2afca`
83
- sha: str
84
- # A unique number for each workflow run within a repository.
85
- # For example, `1658821493`.
86
- run_id: str
87
- # The name of the workflow.
88
- # For example, `My test workflow`.
89
- workflow: str
90
- # The head ref or source branch of the pull request in a workflow run.
91
- # For example, `dd/report-ci`.
92
- head_ref: str | None
93
- # The name of the base ref or target branch of the pull request in a workflow run.
94
- # For example, `main`.
95
- base_ref: str | None
96
- # The branch or tag ref that triggered the workflow run.
97
- # This is only set if a branch or tag is available for the event type.
98
- # For example, `refs/pull/1533/merge`
99
- ref: str | None
100
- # The Schemathesis GitHub Action version.
101
- # For example `v1.0.1`
102
- action_ref: str | None
103
-
104
- @classmethod
105
- def is_set(cls) -> bool:
106
- return os.getenv(cls.variable_name) == "true"
107
-
108
- @classmethod
109
- def from_env(cls) -> GitHubActionsEnvironment:
110
- return cls(
111
- api_url=os.environ["GITHUB_API_URL"],
112
- repository=os.environ["GITHUB_REPOSITORY"],
113
- actor=os.environ["GITHUB_ACTOR"],
114
- sha=os.environ["GITHUB_SHA"],
115
- run_id=os.environ["GITHUB_RUN_ID"],
116
- workflow=os.environ["GITHUB_WORKFLOW"],
117
- head_ref=os.getenv("GITHUB_HEAD_REF"),
118
- base_ref=os.getenv("GITHUB_BASE_REF"),
119
- ref=os.getenv("GITHUB_REF"),
120
- action_ref=os.getenv("SCHEMATHESIS_ACTION_REF"),
121
- )
122
-
123
- def as_env(self) -> dict[str, str | None]:
124
- return {
125
- "GITHUB_API_URL": self.api_url,
126
- "GITHUB_REPOSITORY": self.repository,
127
- "GITHUB_ACTOR": self.actor,
128
- "GITHUB_SHA": self.sha,
129
- "GITHUB_RUN_ID": self.run_id,
130
- "GITHUB_WORKFLOW": self.workflow,
131
- "GITHUB_HEAD_REF": self.head_ref,
132
- "GITHUB_BASE_REF": self.base_ref,
133
- "GITHUB_REF": self.ref,
134
- "SCHEMATHESIS_ACTION_REF": self.action_ref,
135
- }
136
-
137
-
138
- @dataclass
139
- class GitLabCIEnvironment:
140
- """Useful data to capture from GitLab CI environment."""
141
-
142
- provider = CIProvider.GITLAB
143
- variable_name = "GITLAB_CI"
144
- verbose_name = "GitLab CI"
145
- asdict = _asdict
146
-
147
- # GitLab API URL
148
- # For example, `https://gitlab.com/api/v4`
149
- api_v4_url: str
150
- # The ID of the current project.
151
- # For example, `12345678`
152
- project_id: str
153
- # The username of the user who started the job.
154
- # For example, `Stranger6667`
155
- user_login: str
156
- # The commit revision the project is built for.
157
- # For example, `e56e13224f08469841e106449f6467b769e2afca`
158
- commit_sha: str
159
- # NOTE: `commit_branch` and `merge_request_source_branch_name` may mean the same thing, but they are available
160
- # in different context. There are also a couple of `CI_BUILD_*` variables that could be used, but they are
161
- # not documented.
162
- # The commit branch name. Not available in merge request pipelines or tag pipelines.
163
- # For example, `dd/report-ci`.
164
- commit_branch: str | None
165
- # The source branch name of the merge request. Only available in merge request pipelines.
166
- # For example, `dd/report-ci`.
167
- merge_request_source_branch_name: str | None
168
- # The target branch name of the merge request.
169
- # For example, `main`.
170
- merge_request_target_branch_name: str | None
171
- # The project-level internal ID of the merge request.
172
- # For example, `42`.
173
- merge_request_iid: str | None
174
-
175
- @classmethod
176
- def is_set(cls) -> bool:
177
- return os.getenv(cls.variable_name) == "true"
178
-
179
- @classmethod
180
- def from_env(cls) -> GitLabCIEnvironment:
181
- return cls(
182
- api_v4_url=os.environ["CI_API_V4_URL"],
183
- project_id=os.environ["CI_PROJECT_ID"],
184
- user_login=os.environ["GITLAB_USER_LOGIN"],
185
- commit_sha=os.environ["CI_COMMIT_SHA"],
186
- commit_branch=os.getenv("CI_COMMIT_BRANCH"),
187
- merge_request_source_branch_name=os.getenv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"),
188
- merge_request_target_branch_name=os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME"),
189
- merge_request_iid=os.getenv("CI_MERGE_REQUEST_IID"),
190
- )
191
-
192
- def as_env(self) -> dict[str, str | None]:
193
- return {
194
- "CI_API_V4_URL": self.api_v4_url,
195
- "CI_PROJECT_ID": self.project_id,
196
- "GITLAB_USER_LOGIN": self.user_login,
197
- "CI_COMMIT_SHA": self.commit_sha,
198
- "CI_COMMIT_BRANCH": self.commit_branch,
199
- "CI_MERGE_REQUEST_SOURCE_BRANCH_NAME": self.merge_request_source_branch_name,
200
- "CI_MERGE_REQUEST_TARGET_BRANCH_NAME": self.merge_request_target_branch_name,
201
- "CI_MERGE_REQUEST_IID": self.merge_request_iid,
202
- }
@@ -1,133 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import http
5
- import json
6
- from dataclasses import asdict
7
- from typing import TYPE_CHECKING, Any
8
- from urllib.parse import urljoin
9
-
10
- import requests
11
- from requests.adapters import HTTPAdapter, Retry
12
-
13
- from ..constants import USER_AGENT
14
- from .constants import CI_PROVIDER_HEADER, REPORT_CORRELATION_ID_HEADER, REQUEST_TIMEOUT, UPLOAD_SOURCE_HEADER
15
- from .metadata import Metadata, collect_dependency_versions
16
- from .models import (
17
- AnalysisError,
18
- AnalysisResult,
19
- AnalysisSuccess,
20
- AuthResponse,
21
- FailedUploadResponse,
22
- ProjectDetails,
23
- ProjectEnvironment,
24
- Specification,
25
- UploadResponse,
26
- UploadSource,
27
- )
28
-
29
- if TYPE_CHECKING:
30
- from ..runner import probes
31
- from .ci import CIProvider
32
-
33
-
34
- def response_hook(response: requests.Response, **_kwargs: Any) -> None:
35
- if response.status_code != http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
36
- response.raise_for_status()
37
-
38
-
39
- class ServiceClient(requests.Session):
40
- """A more convenient session to send requests to Schemathesis.io."""
41
-
42
- def __init__(self, base_url: str, token: str | None, *, timeout: int = REQUEST_TIMEOUT, verify: bool = True):
43
- super().__init__()
44
- self.timeout = timeout
45
- self.verify = verify
46
- self.base_url = base_url
47
- self.headers["User-Agent"] = USER_AGENT
48
- if token is not None:
49
- self.headers["Authorization"] = f"Bearer {token}"
50
- # Automatically check responses for 4XX and 5XX
51
- self.hooks["response"] = [response_hook] # type: ignore
52
- adapter = HTTPAdapter(max_retries=Retry(5))
53
- self.mount("https://", adapter)
54
- self.mount("http://", adapter)
55
-
56
- def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response: # type: ignore
57
- kwargs.setdefault("timeout", self.timeout)
58
- kwargs.setdefault("verify", self.verify)
59
- # All requests will be done against the base url
60
- url = urljoin(self.base_url, url)
61
- return super().request(method, url, *args, **kwargs)
62
-
63
- def get_api_details(self, name: str) -> ProjectDetails:
64
- """Get information about an API."""
65
- response = self.get(f"/cli/projects/{name}/")
66
- data = response.json()
67
- return ProjectDetails(
68
- environments=[
69
- ProjectEnvironment(
70
- url=environment["url"],
71
- name=environment["name"],
72
- description=environment["description"],
73
- is_default=environment["is_default"],
74
- )
75
- for environment in data["environments"]
76
- ],
77
- specification=Specification(schema=data["specification"]["schema"]),
78
- )
79
-
80
- def login(self, metadata: Metadata) -> AuthResponse:
81
- """Send a login request."""
82
- response = self.post("/auth/cli/login/", json={"metadata": asdict(metadata)})
83
- data = response.json()
84
- return AuthResponse(username=data["username"])
85
-
86
- def upload_report(
87
- self,
88
- report: bytes,
89
- correlation_id: str | None = None,
90
- ci_provider: CIProvider | None = None,
91
- source: UploadSource = UploadSource.DEFAULT,
92
- ) -> UploadResponse | FailedUploadResponse:
93
- """Upload test run report to Schemathesis.io."""
94
- headers = {
95
- "Content-Type": "application/x-gtar",
96
- "X-Checksum-Blake2s256": hashlib.blake2s(report).hexdigest(),
97
- UPLOAD_SOURCE_HEADER: source.value,
98
- }
99
- if correlation_id is not None:
100
- headers[REPORT_CORRELATION_ID_HEADER] = correlation_id
101
- if ci_provider is not None:
102
- headers[CI_PROVIDER_HEADER] = ci_provider.value
103
- # Do not limit the upload timeout
104
- response = self.post("/reports/upload/", report, headers=headers, timeout=None)
105
- data = response.json()
106
- if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
107
- return FailedUploadResponse(detail=data["detail"])
108
- return UploadResponse(message=data["message"], next_url=data["next"], correlation_id=data["correlation_id"])
109
-
110
- def analyze_schema(self, probes: list[probes.ProbeRun] | None, schema: dict[str, Any]) -> AnalysisResult:
111
- """Analyze the API schema."""
112
- # Manual serialization reduces the size of the payload a bit
113
- dependencies = collect_dependency_versions()
114
- if probes is not None:
115
- _probes = [probe.serialize() for probe in probes]
116
- else:
117
- _probes = []
118
- content = json.dumps(
119
- {
120
- "probes": _probes,
121
- "schema": schema,
122
- "dependencies": list(map(asdict, dependencies)),
123
- },
124
- separators=(",", ":"),
125
- )
126
- response = self.post("/cli/analysis/", data=content, headers={"Content-Type": "application/json"}, timeout=None)
127
- if response.status_code == http.HTTPStatus.REQUEST_ENTITY_TOO_LARGE:
128
- try:
129
- message = response.json()["detail"]
130
- except json.JSONDecodeError:
131
- message = response.text
132
- return AnalysisError(message=message)
133
- return AnalysisSuccess.from_dict(response.json())
@@ -1,38 +0,0 @@
1
- import os
2
- import pathlib
3
-
4
- IS_CI = os.getenv("CI") == "true"
5
-
6
- DEFAULT_HOSTNAME = "api.schemathesis.io"
7
- # The main Schemathesis.io API address
8
- DEFAULT_URL = f"https://{DEFAULT_HOSTNAME}/"
9
- DEFAULT_PROTOCOL = "https"
10
- # An HTTP header name to store report correlation id
11
- REPORT_CORRELATION_ID_HEADER = "X-Schemathesis-Report-Correlation-Id"
12
- CI_PROVIDER_HEADER = "X-Schemathesis-CI-Provider"
13
- UPLOAD_SOURCE_HEADER = "X-Schemathesis-Upload-Source"
14
- # A sentinel to signal the worker thread to stop
15
- STOP_MARKER = object()
16
- # Timeout for each API call
17
- REQUEST_TIMEOUT = 1
18
- # The time the main thread will wait for the worker thread to finish its job before exiting
19
- WORKER_FINISH_TIMEOUT = 10.0
20
- # A period between checking the worker thread for events
21
- # Decrease the frequency for CI environment to avoid too much output from the waiting spinner
22
- WORKER_CHECK_PERIOD = 0.1 if IS_CI else 0.005
23
- # Wait until the worker terminates
24
- WORKER_JOIN_TIMEOUT = 10
25
- # Version of the hosts file format
26
- HOSTS_FORMAT_VERSION = "0.1"
27
- # Upload report version
28
- REPORT_FORMAT_VERSION = "1"
29
- # Default path to the hosts file
30
- DEFAULT_HOSTS_PATH = pathlib.Path.home() / ".config/schemathesis/hosts.toml"
31
- TOKEN_ENV_VAR = "SCHEMATHESIS_TOKEN"
32
- HOSTNAME_ENV_VAR = "SCHEMATHESIS_HOSTNAME"
33
- PROTOCOL_ENV_VAR = "SCHEMATHESIS_PROTOCOL"
34
- HOSTS_PATH_ENV_VAR = "SCHEMATHESIS_HOSTS_PATH"
35
- URL_ENV_VAR = "SCHEMATHESIS_URL"
36
- REPORT_ENV_VAR = "SCHEMATHESIS_REPORT"
37
- TELEMETRY_ENV_VAR = "SCHEMATHESIS_TELEMETRY"
38
- DOCKER_IMAGE_ENV_VAR = "SCHEMATHESIS_DOCKER_IMAGE"
@@ -1,61 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import TYPE_CHECKING
5
-
6
- from ..exceptions import format_exception
7
-
8
- if TYPE_CHECKING:
9
- from . import ci
10
-
11
-
12
- class Event:
13
- """Signalling events coming from the Schemathesis.io worker.
14
-
15
- The purpose is to communicate with the thread that writes to stdout.
16
- """
17
-
18
- @property
19
- def status(self) -> str:
20
- return self.__class__.__name__.upper()
21
-
22
-
23
- @dataclass
24
- class Metadata(Event):
25
- """Meta-information about the report."""
26
-
27
- size: int
28
- ci_environment: ci.Environment | None
29
-
30
-
31
- @dataclass
32
- class Completed(Event):
33
- """Report uploaded successfully."""
34
-
35
- message: str
36
- next_url: str
37
-
38
-
39
- @dataclass
40
- class Error(Event):
41
- """Internal error inside the Schemathesis.io handler."""
42
-
43
- exception: Exception
44
-
45
- def get_message(self, include_traceback: bool = False) -> str:
46
- return format_exception(self.exception, include_traceback=include_traceback)
47
-
48
-
49
- @dataclass
50
- class Failed(Event):
51
- """A client-side error which should be displayed to the user."""
52
-
53
- detail: str
54
-
55
-
56
- @dataclass
57
- class Timeout(Event):
58
- """The handler did not finish its work in time.
59
-
60
- This event is not created in the handler itself, but rather in the main thread code to uniform the processing.
61
- """
@@ -1,224 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import base64
4
- import re
5
- from ipaddress import IPv4Network, IPv6Network
6
- from typing import TYPE_CHECKING, Any, Callable
7
-
8
- from ..graphql import nodes
9
- from ..internal.result import Err, Ok, Result
10
- from .models import (
11
- Extension,
12
- GraphQLScalarsExtension,
13
- MediaTypesExtension,
14
- OpenApiStringFormatsExtension,
15
- SchemaPatchesExtension,
16
- StrategyDefinition,
17
- TransformFunctionDefinition,
18
- )
19
-
20
- if TYPE_CHECKING:
21
- from datetime import date, datetime
22
-
23
- from hypothesis import strategies as st
24
-
25
- from ..schemas import BaseSchema
26
-
27
-
28
- def apply(extensions: list[Extension], schema: BaseSchema) -> None:
29
- """Apply the given extensions."""
30
- for extension in extensions:
31
- if isinstance(extension, OpenApiStringFormatsExtension):
32
- _apply_string_formats_extension(extension)
33
- elif isinstance(extension, GraphQLScalarsExtension):
34
- _apply_scalars_extension(extension)
35
- elif isinstance(extension, MediaTypesExtension):
36
- _apply_media_types_extension(extension)
37
- elif isinstance(extension, SchemaPatchesExtension):
38
- _apply_schema_patches_extension(extension, schema)
39
-
40
-
41
- def _apply_simple_extension(
42
- extension: OpenApiStringFormatsExtension | GraphQLScalarsExtension | MediaTypesExtension,
43
- collection: dict[str, Any],
44
- register_strategy: Callable[[str, st.SearchStrategy], None],
45
- ) -> None:
46
- errors = []
47
- for name, value in collection.items():
48
- strategy = strategy_from_definitions(value)
49
- if isinstance(strategy, Err):
50
- errors.append(str(strategy.err()))
51
- else:
52
- register_strategy(name, strategy.ok())
53
-
54
- if errors:
55
- extension.set_error(errors=errors)
56
- else:
57
- extension.set_success()
58
-
59
-
60
- def _apply_string_formats_extension(extension: OpenApiStringFormatsExtension) -> None:
61
- from ..specs.openapi import formats
62
-
63
- _apply_simple_extension(extension, extension.formats, formats.register)
64
-
65
-
66
- def _apply_scalars_extension(extension: GraphQLScalarsExtension) -> None:
67
- from ..specs.graphql import scalars
68
-
69
- _apply_simple_extension(extension, extension.scalars, scalars.scalar)
70
-
71
-
72
- def _apply_media_types_extension(extension: MediaTypesExtension) -> None:
73
- from ..specs.openapi import media_types
74
-
75
- _apply_simple_extension(extension, extension.media_types, media_types.register_media_type)
76
-
77
-
78
- def _find_built_in_strategy(name: str) -> st.SearchStrategy | None:
79
- """Find a built-in Hypothesis strategy by its name."""
80
- from hypothesis import provisional as pr
81
- from hypothesis import strategies as st
82
-
83
- for module in (st, pr):
84
- if hasattr(module, name):
85
- return getattr(module, name)
86
- return None
87
-
88
-
89
- def _apply_schema_patches_extension(extension: SchemaPatchesExtension, schema: BaseSchema) -> None:
90
- """Apply a set of patches to the schema."""
91
- for patch in extension.patches:
92
- current: dict[str, Any] | list = schema.raw_schema
93
- operation = patch["operation"]
94
- path = patch["path"]
95
- for part in path[:-1]:
96
- if isinstance(current, dict):
97
- if not isinstance(part, str):
98
- extension.set_error([f"Invalid path: {path}"])
99
- return
100
- current = current.setdefault(part, {})
101
- elif isinstance(current, list):
102
- if not isinstance(part, int):
103
- extension.set_error([f"Invalid path: {path}"])
104
- return
105
- try:
106
- current = current[part]
107
- except IndexError:
108
- extension.set_error([f"Invalid path: {path}"])
109
- return
110
- if operation == "add":
111
- # Add or replace the value at the target location.
112
- current[path[-1]] = patch["value"] # type: ignore
113
- elif operation == "remove":
114
- # Remove the item at the target location if it exists.
115
- if path:
116
- last = path[-1]
117
- if isinstance(current, dict) and isinstance(last, str) and last in current:
118
- del current[last]
119
- elif isinstance(current, list) and isinstance(last, int) and len(current) > last:
120
- del current[last]
121
- else:
122
- extension.set_error([f"Invalid path: {path}"])
123
- return
124
- else:
125
- current.clear()
126
-
127
- extension.set_success()
128
-
129
-
130
- def strategy_from_definitions(definitions: list[StrategyDefinition]) -> Result[st.SearchStrategy, Exception]:
131
- from ..generation import combine_strategies
132
-
133
- strategies = []
134
- for definition in definitions:
135
- strategy = _strategy_from_definition(definition)
136
- if isinstance(strategy, Ok):
137
- strategies.append(strategy.ok())
138
- else:
139
- return strategy
140
- return Ok(combine_strategies(strategies))
141
-
142
-
143
- KNOWN_ARGUMENTS = {
144
- "IPv4Network": IPv4Network,
145
- "IPv6Network": IPv6Network,
146
- }
147
-
148
-
149
- def check_regex(regex: str) -> Result[None, Exception]:
150
- try:
151
- re.compile(regex)
152
- except (re.error, OverflowError, RuntimeError):
153
- return Err(ValueError(f"Invalid regex: `{regex}`"))
154
- return Ok(None)
155
-
156
-
157
- def check_sampled_from(elements: list) -> Result[None, Exception]:
158
- if not elements:
159
- return Err(ValueError("Invalid input for `sampled_from`: Cannot sample from a length-zero sequence"))
160
- return Ok(None)
161
-
162
-
163
- STRATEGY_ARGUMENT_CHECKS = {
164
- "from_regex": check_regex,
165
- "sampled_from": check_sampled_from,
166
- }
167
-
168
-
169
- def _strategy_from_definition(definition: StrategyDefinition) -> Result[st.SearchStrategy, Exception]:
170
- base = _find_built_in_strategy(definition.name)
171
- if base is None:
172
- return Err(ValueError(f"Unknown built-in strategy: `{definition.name}`"))
173
- arguments = definition.arguments or {}
174
- arguments = arguments.copy()
175
- for key, value in arguments.items():
176
- if isinstance(value, str):
177
- known = KNOWN_ARGUMENTS.get(value)
178
- if known is not None:
179
- arguments[key] = known
180
- check = STRATEGY_ARGUMENT_CHECKS.get(definition.name)
181
- if check is not None:
182
- check_result = check(**arguments) # type: ignore
183
- if isinstance(check_result, Err):
184
- return check_result
185
- strategy = base(**arguments)
186
- for transform in definition.transforms or []:
187
- if transform["kind"] == "map":
188
- function = _get_map_function(transform)
189
- if isinstance(function, Ok):
190
- strategy = strategy.map(function.ok())
191
- else:
192
- return function
193
- else:
194
- return Err(ValueError(f"Unknown transform kind: {transform['kind']}"))
195
-
196
- return Ok(strategy)
197
-
198
-
199
- def make_strftime(format: str) -> Callable:
200
- def strftime(value: date | datetime) -> str:
201
- return value.strftime(format)
202
-
203
- return strftime
204
-
205
-
206
- def _get_map_function(definition: TransformFunctionDefinition) -> Result[Callable | None, Exception]:
207
- from ..serializers import Binary
208
-
209
- TRANSFORM_FACTORIES: dict[str, Callable] = {
210
- "str": lambda: str,
211
- "base64_encode": lambda: lambda x: Binary(base64.b64encode(x)),
212
- "base64_decode": lambda: lambda x: Binary(base64.b64decode(x)),
213
- "urlsafe_base64_encode": lambda: lambda x: Binary(base64.urlsafe_b64encode(x)),
214
- "strftime": make_strftime,
215
- "GraphQLBoolean": lambda: nodes.Boolean,
216
- "GraphQLFloat": lambda: nodes.Float,
217
- "GraphQLInt": lambda: nodes.Int,
218
- "GraphQLString": lambda: nodes.String,
219
- }
220
- factory = TRANSFORM_FACTORIES.get(definition["name"])
221
- if factory is None:
222
- return Err(ValueError(f"Unknown transform: {definition['name']}"))
223
- arguments = definition.get("arguments", {})
224
- return Ok(factory(**arguments))