schemathesis 3.39.16__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.
- schemathesis/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +233 -307
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -717
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.16.dist-info/METADATA +0 -293
- schemathesis-3.39.16.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
schemathesis/service/ci.py
DELETED
@@ -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
|
-
}
|
schemathesis/service/client.py
DELETED
@@ -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"
|
schemathesis/service/events.py
DELETED
@@ -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))
|