schemathesis 4.0.0a6__py3-none-any.whl → 4.0.0a8__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 +4 -0
- schemathesis/cli/commands/run/__init__.py +12 -0
- schemathesis/cli/commands/run/validation.py +8 -0
- schemathesis/core/failures.py +1 -1
- schemathesis/engine/phases/__init__.py +1 -1
- schemathesis/engine/phases/unit/__init__.py +42 -37
- schemathesis/generation/__init__.py +1 -0
- schemathesis/generation/coverage.py +37 -2
- schemathesis/generation/hypothesis/builder.py +40 -8
- schemathesis/hooks.py +1 -1
- schemathesis/pytest/plugin.py +6 -14
- schemathesis/schemas.py +18 -1
- schemathesis/specs/openapi/checks.py +1 -1
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +1 -1
- schemathesis/specs/openapi/negative/mutations.py +1 -1
- schemathesis/specs/openapi/parameters.py +3 -0
- schemathesis/specs/openapi/references.py +4 -1
- schemathesis/specs/openapi/schemas.py +31 -20
- {schemathesis-4.0.0a6.dist-info → schemathesis-4.0.0a8.dist-info}/METADATA +1 -1
- {schemathesis-4.0.0a6.dist-info → schemathesis-4.0.0a8.dist-info}/RECORD +24 -24
- {schemathesis-4.0.0a6.dist-info → schemathesis-4.0.0a8.dist-info}/WHEEL +0 -0
- {schemathesis-4.0.0a6.dist-info → schemathesis-4.0.0a8.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.0.0a6.dist-info → schemathesis-4.0.0a8.dist-info}/licenses/LICENSE +0 -0
schemathesis/__init__.py
CHANGED
@@ -8,6 +8,8 @@ from schemathesis.core.version import SCHEMATHESIS_VERSION
|
|
8
8
|
from schemathesis.generation import GenerationConfig, GenerationMode, HeaderConfig
|
9
9
|
from schemathesis.generation.case import Case
|
10
10
|
from schemathesis.generation.targets import TargetContext, TargetFunction, target
|
11
|
+
from schemathesis.hooks import HookContext
|
12
|
+
from schemathesis.schemas import BaseSchema
|
11
13
|
|
12
14
|
__version__ = SCHEMATHESIS_VERSION
|
13
15
|
|
@@ -26,6 +28,8 @@ __all__ = [
|
|
26
28
|
"Response",
|
27
29
|
"TargetContext",
|
28
30
|
"TargetFunction",
|
31
|
+
"HookContext",
|
32
|
+
"BaseSchema",
|
29
33
|
"__version__",
|
30
34
|
"auth",
|
31
35
|
"check",
|
@@ -305,6 +305,16 @@ DEFAULT_PHASES = ("examples", "coverage", "fuzzing", "stateful")
|
|
305
305
|
multiple=True,
|
306
306
|
metavar="FEATURES",
|
307
307
|
)
|
308
|
+
@grouped_option(
|
309
|
+
"--experimental-coverage-unexpected-methods",
|
310
|
+
"coverage_unexpected_methods",
|
311
|
+
help="HTTP methods to use when generating test cases with methods not specified in the API during the coverage phase.",
|
312
|
+
type=CsvChoice(["get", "put", "post", "delete", "options", "head", "patch", "trace"], case_sensitive=False),
|
313
|
+
callback=validation.convert_http_methods,
|
314
|
+
metavar="",
|
315
|
+
default=None,
|
316
|
+
envvar="SCHEMATHESIS_EXPERIMENTAL_COVERAGE_UNEXPECTED_METHODS",
|
317
|
+
)
|
308
318
|
@grouped_option(
|
309
319
|
"--experimental-missing-required-header-allowed-statuses",
|
310
320
|
"missing_required_header_allowed_statuses",
|
@@ -489,6 +499,7 @@ def run(
|
|
489
499
|
set_cookie: dict[str, str],
|
490
500
|
set_path: dict[str, str],
|
491
501
|
experiments: list,
|
502
|
+
coverage_unexpected_methods: set[str] | None,
|
492
503
|
missing_required_header_allowed_statuses: list[str],
|
493
504
|
positive_data_acceptance_allowed_statuses: list[str],
|
494
505
|
negative_data_rejection_allowed_statuses: list[str],
|
@@ -655,6 +666,7 @@ def run(
|
|
655
666
|
graphql_allow_null=generation_graphql_allow_null,
|
656
667
|
codec=generation_codec,
|
657
668
|
with_security_parameters=generation_with_security_parameters,
|
669
|
+
unexpected_methods=coverage_unexpected_methods,
|
658
670
|
),
|
659
671
|
max_failures=max_failures,
|
660
672
|
continue_on_failure=continue_on_failure,
|
@@ -270,6 +270,14 @@ def reduce_list(ctx: click.core.Context, param: click.core.Parameter, value: tup
|
|
270
270
|
return reduce(operator.iadd, value, [])
|
271
271
|
|
272
272
|
|
273
|
+
def convert_http_methods(
|
274
|
+
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
275
|
+
) -> set[str] | None:
|
276
|
+
if value is None:
|
277
|
+
return value
|
278
|
+
return {item.lower() for item in value}
|
279
|
+
|
280
|
+
|
273
281
|
def convert_status_codes(
|
274
282
|
ctx: click.core.Context, param: click.core.Parameter, value: list[str] | None
|
275
283
|
) -> list[str] | None:
|
schemathesis/core/failures.py
CHANGED
@@ -51,43 +51,48 @@ def execute(engine: EngineContext, phase: Phase) -> events.EventGenerator:
|
|
51
51
|
status = None
|
52
52
|
is_executed = False
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
if event
|
75
|
-
status
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
54
|
+
try:
|
55
|
+
with WorkerPool(
|
56
|
+
workers_num=workers_num,
|
57
|
+
producer=producer,
|
58
|
+
worker_factory=worker_task,
|
59
|
+
ctx=engine,
|
60
|
+
mode=mode,
|
61
|
+
phase=phase.name,
|
62
|
+
suite_id=suite_started.id,
|
63
|
+
) as pool:
|
64
|
+
try:
|
65
|
+
while True:
|
66
|
+
try:
|
67
|
+
event = pool.events_queue.get(timeout=WORKER_TIMEOUT)
|
68
|
+
is_executed = True
|
69
|
+
if engine.is_interrupted:
|
70
|
+
raise KeyboardInterrupt
|
71
|
+
yield event
|
72
|
+
if isinstance(event, events.NonFatalError):
|
73
|
+
status = Status.ERROR
|
74
|
+
if isinstance(event, events.ScenarioFinished):
|
75
|
+
if event.status != Status.SKIP and (status is None or status < event.status):
|
76
|
+
status = event.status
|
77
|
+
if event.status in (Status.ERROR, Status.FAILURE):
|
78
|
+
engine.control.count_failure()
|
79
|
+
if isinstance(event, events.Interrupted) or engine.is_interrupted:
|
80
|
+
status = Status.INTERRUPTED
|
81
|
+
engine.stop()
|
82
|
+
if engine.has_to_stop:
|
83
|
+
break # type: ignore[unreachable]
|
84
|
+
except queue.Empty:
|
85
|
+
if all(not worker.is_alive() for worker in pool.workers):
|
86
|
+
break
|
87
|
+
continue
|
88
|
+
except KeyboardInterrupt:
|
89
|
+
# Soft stop, waiting for workers to terminate
|
90
|
+
engine.stop()
|
91
|
+
status = Status.INTERRUPTED
|
92
|
+
yield events.Interrupted(phase=phase.name)
|
93
|
+
except KeyboardInterrupt:
|
94
|
+
# Hard stop, don't wait for worker threads
|
95
|
+
pass
|
91
96
|
|
92
97
|
if not is_executed:
|
93
98
|
phase.skip_reason = PhaseSkipReason.NOTHING_TO_TEST
|
@@ -150,7 +150,7 @@ class CoverageContext:
|
|
150
150
|
|
151
151
|
def is_valid_for_location(self, value: Any) -> bool:
|
152
152
|
if self.location in ("header", "cookie") and isinstance(value, str):
|
153
|
-
return is_latin_1_encodable(value) and not has_invalid_characters("", value)
|
153
|
+
return not value or (is_latin_1_encodable(value) and not has_invalid_characters("", value))
|
154
154
|
return True
|
155
155
|
|
156
156
|
def generate_from(self, strategy: st.SearchStrategy) -> Any:
|
@@ -437,6 +437,36 @@ def cover_schema_iter(
|
|
437
437
|
elif key == "required":
|
438
438
|
template = template or ctx.generate_from_schema(_get_template_schema(schema, "object"))
|
439
439
|
yield from _negative_required(ctx, template, value)
|
440
|
+
elif key == "maxItems" and isinstance(value, int) and value < BUFFER_SIZE:
|
441
|
+
try:
|
442
|
+
# Force the array to have one more item than allowed
|
443
|
+
new_schema = {**schema, "minItems": value + 1, "maxItems": value + 1, "type": "array"}
|
444
|
+
array_value = ctx.generate_from_schema(new_schema)
|
445
|
+
k = _to_hashable_key(array_value)
|
446
|
+
if k not in seen:
|
447
|
+
yield NegativeValue(
|
448
|
+
array_value,
|
449
|
+
description="Array with more items than allowed by maxItems",
|
450
|
+
location=ctx.current_path,
|
451
|
+
)
|
452
|
+
seen.add(k)
|
453
|
+
except (InvalidArgument, Unsatisfiable):
|
454
|
+
pass
|
455
|
+
elif key == "minItems" and isinstance(value, int) and value > 0:
|
456
|
+
try:
|
457
|
+
# Force the array to have one less item than the minimum
|
458
|
+
new_schema = {**schema, "minItems": value - 1, "maxItems": value - 1, "type": "array"}
|
459
|
+
array_value = ctx.generate_from_schema(new_schema)
|
460
|
+
k = _to_hashable_key(array_value)
|
461
|
+
if k not in seen:
|
462
|
+
yield NegativeValue(
|
463
|
+
array_value,
|
464
|
+
description="Array with fewer items than allowed by minItems",
|
465
|
+
location=ctx.current_path,
|
466
|
+
)
|
467
|
+
seen.add(k)
|
468
|
+
except (InvalidArgument, Unsatisfiable):
|
469
|
+
pass
|
440
470
|
elif (
|
441
471
|
key == "additionalProperties"
|
442
472
|
and not value
|
@@ -770,7 +800,12 @@ def _negative_enum(
|
|
770
800
|
_hashed = _to_hashable_key(x)
|
771
801
|
return _hashed not in seen
|
772
802
|
|
773
|
-
strategy = (
|
803
|
+
strategy = (
|
804
|
+
st.text(alphabet=st.characters(min_codepoint=65, max_codepoint=122, categories=["L"]), min_size=3)
|
805
|
+
| st.none()
|
806
|
+
| st.booleans()
|
807
|
+
| NUMERIC_STRATEGY
|
808
|
+
).filter(is_not_in_value)
|
774
809
|
value = ctx.generate_from(strategy)
|
775
810
|
yield NegativeValue(value, description="Invalid enum value", location=ctx.current_path)
|
776
811
|
hashed = _to_hashable_key(value)
|
@@ -1,9 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
from dataclasses import dataclass, field
|
4
5
|
from enum import Enum
|
5
6
|
from functools import wraps
|
6
7
|
from itertools import combinations
|
8
|
+
import os
|
7
9
|
from time import perf_counter
|
8
10
|
from typing import Any, Callable, Generator, Mapping
|
9
11
|
|
@@ -15,7 +17,7 @@ from jsonschema.exceptions import SchemaError
|
|
15
17
|
|
16
18
|
from schemathesis import auths
|
17
19
|
from schemathesis.auths import AuthStorage, AuthStorageMark
|
18
|
-
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types
|
20
|
+
from schemathesis.core import NOT_SET, NotSet, SpecificationFeature, media_types, string_to_boolean
|
19
21
|
from schemathesis.core.errors import InvalidSchema, SerializationNotPossible
|
20
22
|
from schemathesis.core.marks import Mark
|
21
23
|
from schemathesis.core.transport import prepare_urlencoded
|
@@ -38,7 +40,7 @@ from schemathesis.schemas import APIOperation, ParameterSet
|
|
38
40
|
setup()
|
39
41
|
|
40
42
|
|
41
|
-
class HypothesisTestMode(Enum):
|
43
|
+
class HypothesisTestMode(str, Enum):
|
42
44
|
EXAMPLES = "examples"
|
43
45
|
COVERAGE = "coverage"
|
44
46
|
FUZZING = "fuzzing"
|
@@ -120,15 +122,23 @@ def create_test(
|
|
120
122
|
):
|
121
123
|
hypothesis_test = add_examples(hypothesis_test, operation, hook_dispatcher=hook_dispatcher, **strategy_kwargs)
|
122
124
|
|
125
|
+
disable_coverage = string_to_boolean(os.getenv("SCHEMATHESIS_DISABLE_COVERAGE", ""))
|
126
|
+
|
123
127
|
if (
|
124
|
-
|
128
|
+
not disable_coverage
|
129
|
+
and HypothesisTestMode.COVERAGE in config.modes
|
125
130
|
and Phase.explicit in settings.phases
|
126
131
|
and specification.supports_feature(SpecificationFeature.COVERAGE)
|
127
132
|
and not config.given_args
|
128
133
|
and not config.given_kwargs
|
129
134
|
):
|
130
135
|
hypothesis_test = add_coverage(
|
131
|
-
hypothesis_test,
|
136
|
+
hypothesis_test,
|
137
|
+
operation,
|
138
|
+
config.generation.modes,
|
139
|
+
auth_storage,
|
140
|
+
config.as_strategy_kwargs,
|
141
|
+
config.generation.unexpected_methods,
|
132
142
|
)
|
133
143
|
|
134
144
|
setattr(hypothesis_test, SETTINGS_ATTRIBUTE_NAME, settings)
|
@@ -153,7 +163,24 @@ def create_base_test(
|
|
153
163
|
__tracebackhide__ = True
|
154
164
|
return test_function(*args, **kwargs)
|
155
165
|
|
156
|
-
|
166
|
+
funcobj = hypothesis.given(*args, **{**kwargs, "case": strategy})(test_wrapper)
|
167
|
+
|
168
|
+
if asyncio.iscoroutinefunction(test_function):
|
169
|
+
funcobj.hypothesis.inner_test = make_async_test(test_function) # type: ignore
|
170
|
+
return funcobj
|
171
|
+
|
172
|
+
|
173
|
+
def make_async_test(test: Callable) -> Callable:
|
174
|
+
def async_run(*args: Any, **kwargs: Any) -> None:
|
175
|
+
try:
|
176
|
+
loop = asyncio.get_event_loop()
|
177
|
+
except RuntimeError:
|
178
|
+
loop = asyncio.new_event_loop()
|
179
|
+
coro = test(*args, **kwargs)
|
180
|
+
future = asyncio.ensure_future(coro, loop=loop)
|
181
|
+
loop.run_until_complete(future)
|
182
|
+
|
183
|
+
return async_run
|
157
184
|
|
158
185
|
|
159
186
|
def add_examples(
|
@@ -215,6 +242,7 @@ def add_coverage(
|
|
215
242
|
generation_modes: list[GenerationMode],
|
216
243
|
auth_storage: AuthStorage | None,
|
217
244
|
as_strategy_kwargs: dict[str, Any],
|
245
|
+
unexpected_methods: set[str] | None = None,
|
218
246
|
) -> Callable:
|
219
247
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
220
248
|
|
@@ -227,7 +255,7 @@ def add_coverage(
|
|
227
255
|
for container in LOCATION_TO_CONTAINER.values()
|
228
256
|
if container in as_strategy_kwargs
|
229
257
|
}
|
230
|
-
for case in _iter_coverage_cases(operation, generation_modes):
|
258
|
+
for case in _iter_coverage_cases(operation, generation_modes, unexpected_methods):
|
231
259
|
if case.media_type and operation.schema.transport.get_first_matching_media_type(case.media_type) is None:
|
232
260
|
continue
|
233
261
|
adjust_urlencoded_payload(case)
|
@@ -362,7 +390,9 @@ def _stringify_value(val: Any, container_name: str) -> Any:
|
|
362
390
|
|
363
391
|
|
364
392
|
def _iter_coverage_cases(
|
365
|
-
operation: APIOperation,
|
393
|
+
operation: APIOperation,
|
394
|
+
generation_modes: list[GenerationMode],
|
395
|
+
unexpected_methods: set[str] | None = None,
|
366
396
|
) -> Generator[Case, None, None]:
|
367
397
|
from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
|
368
398
|
from schemathesis.specs.openapi.examples import find_in_responses, find_matching_in_responses
|
@@ -374,6 +404,8 @@ def _iter_coverage_cases(
|
|
374
404
|
|
375
405
|
instant = Instant()
|
376
406
|
responses = find_in_responses(operation)
|
407
|
+
# NOTE: The HEAD method is excluded
|
408
|
+
unexpected_methods = unexpected_methods or {"get", "put", "post", "delete", "options", "patch", "trace"}
|
377
409
|
for parameter in operation.iter_parameters():
|
378
410
|
location = parameter.location
|
379
411
|
name = parameter.name
|
@@ -488,7 +520,7 @@ def _iter_coverage_cases(
|
|
488
520
|
)
|
489
521
|
if GenerationMode.NEGATIVE in generation_modes:
|
490
522
|
# Generate HTTP methods that are not specified in the spec
|
491
|
-
methods =
|
523
|
+
methods = unexpected_methods - set(operation.schema[operation.path])
|
492
524
|
for method in sorted(methods):
|
493
525
|
instant = Instant()
|
494
526
|
data = template.unmodified()
|
schemathesis/hooks.py
CHANGED
schemathesis/pytest/plugin.py
CHANGED
@@ -108,7 +108,12 @@ class SchemathesisCase(PyCollector):
|
|
108
108
|
This implementation is based on the original one in pytest, but with slight adjustments
|
109
109
|
to produce tests out of hypothesis ones.
|
110
110
|
"""
|
111
|
-
from schemathesis.generation.hypothesis.builder import
|
111
|
+
from schemathesis.generation.hypothesis.builder import (
|
112
|
+
HypothesisTestConfig,
|
113
|
+
HypothesisTestMode,
|
114
|
+
create_test,
|
115
|
+
make_async_test,
|
116
|
+
)
|
112
117
|
|
113
118
|
is_trio_test = False
|
114
119
|
for mark in getattr(self.test_function, "pytestmark", []):
|
@@ -221,19 +226,6 @@ class SchemathesisCase(PyCollector):
|
|
221
226
|
pytest.fail("Error during collection")
|
222
227
|
|
223
228
|
|
224
|
-
def make_async_test(test: Callable) -> Callable:
|
225
|
-
def async_run(*args: Any, **kwargs: Any) -> None:
|
226
|
-
try:
|
227
|
-
loop = asyncio.get_event_loop()
|
228
|
-
except RuntimeError:
|
229
|
-
loop = asyncio.new_event_loop()
|
230
|
-
coro = test(*args, **kwargs)
|
231
|
-
future = asyncio.ensure_future(coro, loop=loop)
|
232
|
-
loop.run_until_complete(future)
|
233
|
-
|
234
|
-
return async_run
|
235
|
-
|
236
|
-
|
237
229
|
@hookimpl(hookwrapper=True) # type:ignore
|
238
230
|
def pytest_pycollect_makeitem(collector: nodes.Collector, name: str, obj: Any) -> Generator[None, Any, None]:
|
239
231
|
"""Switch to a different collector if the test is parametrized marked by schemathesis."""
|
schemathesis/schemas.py
CHANGED
@@ -14,7 +14,7 @@ from typing import (
|
|
14
14
|
NoReturn,
|
15
15
|
TypeVar,
|
16
16
|
)
|
17
|
-
from urllib.parse import quote, unquote, urljoin, urlsplit, urlunsplit
|
17
|
+
from urllib.parse import quote, unquote, urljoin, urlparse, urlsplit, urlunsplit
|
18
18
|
|
19
19
|
from schemathesis import transport
|
20
20
|
from schemathesis.core import NOT_SET, NotSet
|
@@ -436,6 +436,8 @@ class BaseSchema(Mapping):
|
|
436
436
|
app: Any | NotSet = NOT_SET,
|
437
437
|
) -> Self:
|
438
438
|
if not isinstance(base_url, NotSet):
|
439
|
+
if base_url is not None:
|
440
|
+
validate_base_url(base_url)
|
439
441
|
self.base_url = base_url
|
440
442
|
if not isinstance(location, NotSet):
|
441
443
|
self.location = location
|
@@ -453,6 +455,21 @@ class BaseSchema(Mapping):
|
|
453
455
|
return self
|
454
456
|
|
455
457
|
|
458
|
+
INVALID_BASE_URL_MESSAGE = (
|
459
|
+
"The provided base URL is invalid. This URL serves as a prefix for all API endpoints you want to test. "
|
460
|
+
"Make sure it is a properly formatted URL."
|
461
|
+
)
|
462
|
+
|
463
|
+
|
464
|
+
def validate_base_url(value: str) -> None:
|
465
|
+
try:
|
466
|
+
netloc = urlparse(value).netloc
|
467
|
+
except ValueError as exc:
|
468
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE) from exc
|
469
|
+
if value and not netloc:
|
470
|
+
raise ValueError(INVALID_BASE_URL_MESSAGE)
|
471
|
+
|
472
|
+
|
456
473
|
@dataclass
|
457
474
|
class APIOperationMap(Mapping):
|
458
475
|
_schema: BaseSchema
|
@@ -20,7 +20,7 @@ from .utils import can_negate
|
|
20
20
|
T = TypeVar("T")
|
21
21
|
|
22
22
|
|
23
|
-
class MutationResult(enum.Enum):
|
23
|
+
class MutationResult(int, enum.Enum):
|
24
24
|
"""The result of applying some mutation to some schema.
|
25
25
|
|
26
26
|
Failing to mutate something means that by applying some mutation, it is not possible to change
|
@@ -134,6 +134,7 @@ class OpenAPI20Parameter(OpenAPIParameter):
|
|
134
134
|
"multipleOf",
|
135
135
|
"example",
|
136
136
|
"examples",
|
137
|
+
"default",
|
137
138
|
)
|
138
139
|
|
139
140
|
|
@@ -178,6 +179,7 @@ class OpenAPI30Parameter(OpenAPIParameter):
|
|
178
179
|
"format",
|
179
180
|
"example",
|
180
181
|
"examples",
|
182
|
+
"default",
|
181
183
|
)
|
182
184
|
|
183
185
|
def from_open_api_to_json_schema(self, operation: APIOperation, open_api_schema: dict[str, Any]) -> dict[str, Any]:
|
@@ -230,6 +232,7 @@ class OpenAPI20Body(OpenAPIBody, OpenAPI20Parameter):
|
|
230
232
|
"additionalProperties",
|
231
233
|
"example",
|
232
234
|
"examples",
|
235
|
+
"default",
|
233
236
|
)
|
234
237
|
# NOTE. For Open API 2.0 bodies, we still give `x-example` precedence over the schema-level `example` field to keep
|
235
238
|
# the precedence rules consistent.
|
@@ -139,7 +139,10 @@ class ConvertingResolver(InliningResolver):
|
|
139
139
|
def resolve(self, ref: str) -> tuple[str, Any]:
|
140
140
|
url, document = super().resolve(ref)
|
141
141
|
document = to_json_schema_recursive(
|
142
|
-
document,
|
142
|
+
document,
|
143
|
+
nullable_name=self.nullable_name,
|
144
|
+
is_response_schema=self.is_response_schema,
|
145
|
+
update_quantifiers=False,
|
143
146
|
)
|
144
147
|
return url, document
|
145
148
|
|
@@ -1022,24 +1022,30 @@ class SwaggerV20(BaseOpenAPISchema):
|
|
1022
1022
|
content_types = self.get_request_payload_content_types(operation)
|
1023
1023
|
is_multipart = "multipart/form-data" in content_types
|
1024
1024
|
|
1025
|
-
|
1026
|
-
if isinstance(file_value, list):
|
1027
|
-
for item in file_value:
|
1028
|
-
files.append((name, (None, item)))
|
1029
|
-
else:
|
1030
|
-
files.append((name, file_value))
|
1025
|
+
known_fields: dict[str, dict] = {}
|
1031
1026
|
|
1032
1027
|
for parameter in operation.body:
|
1033
1028
|
if isinstance(parameter, OpenAPI20CompositeBody):
|
1034
1029
|
for form_parameter in parameter.definition:
|
1035
|
-
name = form_parameter.
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1030
|
+
known_fields[form_parameter.name] = form_parameter.definition
|
1031
|
+
|
1032
|
+
def add_file(name: str, value: Any) -> None:
|
1033
|
+
if isinstance(value, list):
|
1034
|
+
for item in value:
|
1035
|
+
files.append((name, (None, item)))
|
1036
|
+
else:
|
1037
|
+
files.append((name, value))
|
1038
|
+
|
1039
|
+
for name, value in form_data.items():
|
1040
|
+
param_def = known_fields.get(name)
|
1041
|
+
if param_def:
|
1042
|
+
if param_def.get("type") == "file" or is_multipart:
|
1043
|
+
add_file(name, value)
|
1044
|
+
else:
|
1045
|
+
data[name] = value
|
1046
|
+
else:
|
1047
|
+
# Unknown field — treat it as a file (safe default under multipart/form-data)
|
1048
|
+
add_file(name, value)
|
1043
1049
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
1044
1050
|
return files or None, data or None
|
1045
1051
|
|
@@ -1202,14 +1208,19 @@ class OpenApi30(SwaggerV20):
|
|
1202
1208
|
break
|
1203
1209
|
else:
|
1204
1210
|
raise InternalError("No 'multipart/form-data' media type found in the schema")
|
1205
|
-
for name,
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1211
|
+
for name, value in form_data.items():
|
1212
|
+
property_schema = (schema or {}).get("properties", {}).get(name)
|
1213
|
+
if property_schema:
|
1214
|
+
if isinstance(value, list):
|
1215
|
+
files.extend([(name, item) for item in value])
|
1209
1216
|
elif property_schema.get("format") in ("binary", "base64"):
|
1210
|
-
files.append((name,
|
1217
|
+
files.append((name, value))
|
1211
1218
|
else:
|
1212
|
-
files.append((name, (None,
|
1219
|
+
files.append((name, (None, value)))
|
1220
|
+
elif isinstance(value, list):
|
1221
|
+
files.extend([(name, item) for item in value])
|
1222
|
+
else:
|
1223
|
+
files.append((name, (None, value)))
|
1213
1224
|
# `None` is the default value for `files` and `data` arguments in `requests.request`
|
1214
1225
|
return files or None, None
|
1215
1226
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemathesis
|
3
|
-
Version: 4.0.
|
3
|
+
Version: 4.0.0a8
|
4
4
|
Summary: Property-based testing framework for Open API and GraphQL based apps
|
5
5
|
Project-URL: Documentation, https://schemathesis.readthedocs.io/en/stable/
|
6
6
|
Project-URL: Changelog, https://schemathesis.readthedocs.io/en/stable/changelog.html
|
@@ -1,18 +1,18 @@
|
|
1
|
-
schemathesis/__init__.py,sha256=
|
1
|
+
schemathesis/__init__.py,sha256=S9MD8cGyXWihyQikye9mSBpvrfUJbOItD5yr65vkx6A,1263
|
2
2
|
schemathesis/auths.py,sha256=t-YuPyoLqL7jlRUH-45JxO7Ir3pYxpe31CRmNIJh7rI,15423
|
3
3
|
schemathesis/checks.py,sha256=B5-ROnjvvwpaqgj_iQ7eCjGqvRRVT30eWNPLKmwdrM8,5084
|
4
4
|
schemathesis/errors.py,sha256=VSZ-h9Bt7QvrvywOGB-MoHCshR8OWJegYlBxfVh5Vuw,899
|
5
5
|
schemathesis/filters.py,sha256=CzVPnNSRLNgvLlU5_WssPEC0wpdQi0dMvDpHSQbAlkE,13577
|
6
|
-
schemathesis/hooks.py,sha256=
|
6
|
+
schemathesis/hooks.py,sha256=ZSGEnsLJ7UVezf4CcaJebVkjEpvwgJolJFZo5fjQNDc,13153
|
7
7
|
schemathesis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
-
schemathesis/schemas.py,sha256=
|
8
|
+
schemathesis/schemas.py,sha256=A2qAs1PY9wbRWk6PFnslWyIqzchAhu5oo_MsKL7uF8w,27952
|
9
9
|
schemathesis/cli/__init__.py,sha256=U9gjzWWpiFhaqevPjZbwyTNjABdpvXETI4HgwdGKnvs,877
|
10
10
|
schemathesis/cli/__main__.py,sha256=MWaenjaUTZIfNPFzKmnkTiawUri7DVldtg3mirLwzU8,92
|
11
11
|
schemathesis/cli/constants.py,sha256=rUixnqorraUFDtOu3Nmm1x_k0qbgmW9xW96kQB_fBCQ,338
|
12
12
|
schemathesis/cli/core.py,sha256=Qm5xvpIIMwJDTeR3N3TjKhMCHV5d5Rp0UstVS2GjWgw,459
|
13
13
|
schemathesis/cli/hooks.py,sha256=vTrA8EN99whRns5K5AnExViQ6WL9cak5RGsC-ZBEiJM,1458
|
14
14
|
schemathesis/cli/commands/__init__.py,sha256=FFalEss3D7mnCRO0udtYb65onXSjQCCOv8sOSjqvTTM,1059
|
15
|
-
schemathesis/cli/commands/run/__init__.py,sha256=
|
15
|
+
schemathesis/cli/commands/run/__init__.py,sha256=ACIRF3eP-Za56sY5OMSdLdbrmopPvJblDel4Jl-vBtw,23745
|
16
16
|
schemathesis/cli/commands/run/checks.py,sha256=lLtBCt6NhhQisrWo8aC6i0M3dSXlbjGWTTlOyjzatks,3278
|
17
17
|
schemathesis/cli/commands/run/context.py,sha256=pUwSlS7UwW2cq1nJXfKZFEaWDipsQAElCO4tdv1qYJA,7739
|
18
18
|
schemathesis/cli/commands/run/events.py,sha256=Dj-xvIr-Hkms8kvh4whNwKSk1Q2Hx4NIENi_4A8nQO8,1224
|
@@ -21,7 +21,7 @@ schemathesis/cli/commands/run/filters.py,sha256=MdymOZtzOolvXCNBIdfHbBbWEXVF7Se0
|
|
21
21
|
schemathesis/cli/commands/run/hypothesis.py,sha256=hdEHim_Hc2HwCGxAiRTf4t2OfQf0IeCUhyjNT_btB1o,2553
|
22
22
|
schemathesis/cli/commands/run/loaders.py,sha256=VedoeIE1tgFBqVokWxOoUReAjBl-Zhx87RjCEBtCVfs,4840
|
23
23
|
schemathesis/cli/commands/run/reports.py,sha256=OjyakiV0lpNDBZb1xsb_2HmLtcqhTThPYMpJGXyNNO8,2147
|
24
|
-
schemathesis/cli/commands/run/validation.py,sha256=
|
24
|
+
schemathesis/cli/commands/run/validation.py,sha256=cpGG5hFc4lHVemXrQXRvrlNlqBmMqtvx9yUwbOhc2TI,13008
|
25
25
|
schemathesis/cli/commands/run/handlers/__init__.py,sha256=TPZ3KdGi8m0fjlN0GjA31MAXXn1qI7uU4FtiDwroXZI,1915
|
26
26
|
schemathesis/cli/commands/run/handlers/base.py,sha256=yDsTtCiztLksfk7cRzg8JlaAVOfS-zwK3tsJMOXAFyc,530
|
27
27
|
schemathesis/cli/commands/run/handlers/cassettes.py,sha256=SVk13xPhsQduCpgvvBwzEMDNTju-SHQCW90xTQ6iL1U,18525
|
@@ -40,7 +40,7 @@ schemathesis/core/control.py,sha256=IzwIc8HIAEMtZWW0Q0iXI7T1niBpjvcLlbuwOSmy5O8,
|
|
40
40
|
schemathesis/core/curl.py,sha256=yuaCe_zHLGwUjEeloQi6W3tOA3cGdnHDNI17-5jia0o,1723
|
41
41
|
schemathesis/core/deserialization.py,sha256=ygIj4fNaOd0mJ2IvTsn6bsabBt_2AbSLCz-z9UqfpdQ,2406
|
42
42
|
schemathesis/core/errors.py,sha256=97Fk3udsMaS5xZrco7ZaShqe4W6g2aZ55J7d58HPRac,15881
|
43
|
-
schemathesis/core/failures.py,sha256=
|
43
|
+
schemathesis/core/failures.py,sha256=nt_KJAQnachw4Ey-rZ__P8q6nGJ_YekZiSLc6-PfFW0,8833
|
44
44
|
schemathesis/core/fs.py,sha256=ItQT0_cVwjDdJX9IiI7EnU75NI2H3_DCEyyUjzg_BgI,472
|
45
45
|
schemathesis/core/lazy_import.py,sha256=aMhWYgbU2JOltyWBb32vnWBb6kykOghucEzI_F70yVE,470
|
46
46
|
schemathesis/core/loaders.py,sha256=SQQ-8m64-D2FaOgvwKZLyTtLJuzP3RPo7Ud2BERK1c0,3404
|
@@ -63,24 +63,24 @@ schemathesis/engine/core.py,sha256=DfulRMVTivmZj-wwLekIhuSzLsFnuVPtSg7j9HyWdz0,5
|
|
63
63
|
schemathesis/engine/errors.py,sha256=8PHYsuq2qIEJHm2FDf_UnWa4IDc-DRFTPckLAr22yhE,16895
|
64
64
|
schemathesis/engine/events.py,sha256=gslRAWQKMPqBCQzLDS4wAbsKcVuONSy5SPqimJJJYT4,6250
|
65
65
|
schemathesis/engine/recorder.py,sha256=K3HfMARrT5mPWXPnYebjjcq5CcsBRhMrtZwEL9_Lvtg,8432
|
66
|
-
schemathesis/engine/phases/__init__.py,sha256=
|
66
|
+
schemathesis/engine/phases/__init__.py,sha256=CuTBMaQIsGdtWw400maiwqfIbMyVv5_vHXV-SY5A5NI,2495
|
67
67
|
schemathesis/engine/phases/probes.py,sha256=3M9g3E7CXbDDK_8inuvkRZibCCcoO2Ce5U3lnyTeWXQ,5131
|
68
68
|
schemathesis/engine/phases/stateful/__init__.py,sha256=lWo2RLrutNblHvohTzofQqL22GORwBRA8bf6jvLuGPg,2391
|
69
69
|
schemathesis/engine/phases/stateful/_executor.py,sha256=m1ZMqFUPc4Hdql10l0gF3tpP4JOImSA-XeBd4jg3Ll8,12443
|
70
70
|
schemathesis/engine/phases/stateful/context.py,sha256=SKWsok-tlWbUDagiUmP7cLNW6DsgFDc_Afv0vQfWv6c,2964
|
71
|
-
schemathesis/engine/phases/unit/__init__.py,sha256=
|
71
|
+
schemathesis/engine/phases/unit/__init__.py,sha256=QmtzIgP9KWLo-IY1kMyBqYXPMxFQz-WF2eVTWewqUfI,8174
|
72
72
|
schemathesis/engine/phases/unit/_executor.py,sha256=buMEr7e01SFSeNuEQNGMf4hoiLxX9_sp0JhH4LBAk9M,12928
|
73
73
|
schemathesis/engine/phases/unit/_pool.py,sha256=9OgmFd-ov1AAvcZGquK40PXkGLp7f2qCjZoPZuoZl4A,2529
|
74
74
|
schemathesis/experimental/__init__.py,sha256=jYY3Mq6okqTRTMudPzcaT0JVjzJW5IN_ZVJdGU0stBs,2011
|
75
|
-
schemathesis/generation/__init__.py,sha256=
|
75
|
+
schemathesis/generation/__init__.py,sha256=sWTRPTh-qDNkSfpM9rYI3v8zskH8_wFKUuPRg18fZI8,1627
|
76
76
|
schemathesis/generation/case.py,sha256=Rt5MCUtPVYVQzNyjUx8magocPJpHV1svyuqQSTwUE-I,7306
|
77
|
-
schemathesis/generation/coverage.py,sha256=
|
77
|
+
schemathesis/generation/coverage.py,sha256=vv2dbj_KAaqo8PCwMdyDWLyyAssk-YL5oTU3aCkgB1s,41185
|
78
78
|
schemathesis/generation/meta.py,sha256=36h6m4E7jzLGa8TCvl7eBl_xUWLiRul3qxzexl5cB58,2515
|
79
79
|
schemathesis/generation/modes.py,sha256=t_EvKr2aOXYMsEfdMu4lLF4KCGcX1LVVyvzTkcpJqhk,663
|
80
80
|
schemathesis/generation/overrides.py,sha256=FhqcFoliEvgW6MZyFPYemfLgzKt3Miy8Cud7OMOCb7g,3045
|
81
81
|
schemathesis/generation/targets.py,sha256=_rN2qgxTE2EfvygiN-Fy3WmDnRH0ERohdx3sKRDaYhU,2120
|
82
82
|
schemathesis/generation/hypothesis/__init__.py,sha256=Rl7QwvMBMJI7pBqTydplX6bXC420n0EGQHVm-vZgaYQ,1204
|
83
|
-
schemathesis/generation/hypothesis/builder.py,sha256=
|
83
|
+
schemathesis/generation/hypothesis/builder.py,sha256=cSgFWQG-apIHNdW-PpIBDBjLw4RorihfZ4e_Ln3j2-w,30341
|
84
84
|
schemathesis/generation/hypothesis/examples.py,sha256=6eGaKUEC3elmKsaqfKj1sLvM8EHc-PWT4NRBq4NI0Rs,1409
|
85
85
|
schemathesis/generation/hypothesis/given.py,sha256=sTZR1of6XaHAPWtHx2_WLlZ50M8D5Rjux0GmWkWjDq4,2337
|
86
86
|
schemathesis/generation/hypothesis/reporting.py,sha256=uDVow6Ya8YFkqQuOqRsjbzsbyP4KKfr3jA7ZaY4FuKY,279
|
@@ -99,7 +99,7 @@ schemathesis/pytest/__init__.py,sha256=7W0q-Thcw03IAQfXE_Mo8JPZpUdHJzfu85fjK1Zdf
|
|
99
99
|
schemathesis/pytest/control_flow.py,sha256=F8rAPsPeNv_sJiJgbZYtTpwKWjauZmqFUaKroY2GmQI,217
|
100
100
|
schemathesis/pytest/lazy.py,sha256=g7DpOeQNsjXC03FCG5e1L65iz3zE48qAyaqG81HzCZY,12028
|
101
101
|
schemathesis/pytest/loaders.py,sha256=oQJ78yyuIm3Ye9X7giVjDB1vYfaW5UY5YuhaTLm_ZFU,266
|
102
|
-
schemathesis/pytest/plugin.py,sha256=
|
102
|
+
schemathesis/pytest/plugin.py,sha256=TxbESQy9JPZBaIwUP4BHiIGFzPd2oMWwq_4VqFS_UfI,12067
|
103
103
|
schemathesis/python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
104
104
|
schemathesis/python/asgi.py,sha256=5PyvuTBaivvyPUEi3pwJni91K1kX5Zc0u9c6c1D8a1Q,287
|
105
105
|
schemathesis/python/wsgi.py,sha256=uShAgo_NChbfYaV1117e6UHp0MTg7jaR0Sy_to3Jmf8,219
|
@@ -113,28 +113,28 @@ schemathesis/specs/graphql/validation.py,sha256=-W1Noc1MQmTb4RX-gNXMeU2qkgso4mzV
|
|
113
113
|
schemathesis/specs/openapi/__init__.py,sha256=C5HOsfuDJGq_3mv8CRBvRvb0Diy1p0BFdqyEXMS-loE,238
|
114
114
|
schemathesis/specs/openapi/_cache.py,sha256=HpglmETmZU0RCHxp3DO_sg5_B_nzi54Zuw9vGzzYCxY,4295
|
115
115
|
schemathesis/specs/openapi/_hypothesis.py,sha256=n_39iyz1rt2EdSe-Lyr-3sOIEyJIthnCVR4tGUUvH1c,21328
|
116
|
-
schemathesis/specs/openapi/checks.py,sha256=
|
116
|
+
schemathesis/specs/openapi/checks.py,sha256=i4tVVkK1wLthdmG-zu7EaQLkBxJ2T3FkuHqw0dA4qlA,27742
|
117
117
|
schemathesis/specs/openapi/constants.py,sha256=JqM_FHOenqS_MuUE9sxVQ8Hnw0DNM8cnKDwCwPLhID4,783
|
118
118
|
schemathesis/specs/openapi/converter.py,sha256=lil8IewM5j8tvt4lpA9g_KITvIwx1M96i45DNSHNjoc,3505
|
119
119
|
schemathesis/specs/openapi/definitions.py,sha256=8htclglV3fW6JPBqs59lgM4LnA25Mm9IptXBPb_qUT0,93949
|
120
120
|
schemathesis/specs/openapi/examples.py,sha256=Xvjp60QUcLaeGsJRbi2i6XM15_4uO0ceVoClIaJehiE,21062
|
121
121
|
schemathesis/specs/openapi/formats.py,sha256=ViVF3aFeFI1ctwGQbiRDXhU3so82P0BCaF2aDDbUUm8,2816
|
122
122
|
schemathesis/specs/openapi/media_types.py,sha256=ADedOaNWjbAtAekyaKmNj9fY6zBTeqcNqBEjN0EWNhI,1014
|
123
|
-
schemathesis/specs/openapi/parameters.py,sha256=
|
123
|
+
schemathesis/specs/openapi/parameters.py,sha256=tVL61gDe9A8_jwoVKZZvpXKPerMyq7vkAvwdMsi44TI,14622
|
124
124
|
schemathesis/specs/openapi/patterns.py,sha256=NLnGybcana_kYLVKVEjkEyAzdClAV0xKe4Oy4NVayMI,12834
|
125
|
-
schemathesis/specs/openapi/references.py,sha256=
|
126
|
-
schemathesis/specs/openapi/schemas.py,sha256=
|
125
|
+
schemathesis/specs/openapi/references.py,sha256=c8Ufa8hp6Dyf-gPn5lpmyqF_GtqXIBWoKkj3bk3WaPA,8871
|
126
|
+
schemathesis/specs/openapi/schemas.py,sha256=zfGPFWnaI9_W8F8E8qCTzuYQRE5yDuGx7WGW4EH-QgI,55020
|
127
127
|
schemathesis/specs/openapi/security.py,sha256=6UWYMhL-dPtkTineqqBFNKca1i4EuoTduw-EOLeE0aQ,7149
|
128
128
|
schemathesis/specs/openapi/serialization.py,sha256=VdDLmeHqxlWM4cxQQcCkvrU6XurivolwEEaT13ohelA,11972
|
129
129
|
schemathesis/specs/openapi/utils.py,sha256=ER4vJkdFVDIE7aKyxyYatuuHVRNutytezgE52pqZNE8,900
|
130
130
|
schemathesis/specs/openapi/expressions/__init__.py,sha256=hfuRtXD75tQFhzSo6QgDZ3zByyWeZRKevB8edszAVj4,2272
|
131
131
|
schemathesis/specs/openapi/expressions/errors.py,sha256=YLVhps-sYcslgVaahfcUYxUSHlIfWL-rQMeT5PZSMZ8,219
|
132
132
|
schemathesis/specs/openapi/expressions/extractors.py,sha256=Py3of3_vBACP4ljiZIcgd-xQCrWIpcMsfQFc0EtAUoA,470
|
133
|
-
schemathesis/specs/openapi/expressions/lexer.py,sha256=
|
134
|
-
schemathesis/specs/openapi/expressions/nodes.py,sha256=
|
133
|
+
schemathesis/specs/openapi/expressions/lexer.py,sha256=KFA8Z-Kh1IYUpKgwAnDtEucN9YLLpnFR1GQl8KddWlA,3987
|
134
|
+
schemathesis/specs/openapi/expressions/nodes.py,sha256=63LC4mQHy3a0_tKiGIVWaUHu9L9IWilq6R004GLpjyY,4077
|
135
135
|
schemathesis/specs/openapi/expressions/parser.py,sha256=e-ZxshrGE_5CVbgcZLYgdGSjdifgyzgKkLQp0dI0cJY,4503
|
136
136
|
schemathesis/specs/openapi/negative/__init__.py,sha256=60QqVBTXPTsAojcf7GDs7v8WbOE_k3g_VC_DBeQUqBw,3749
|
137
|
-
schemathesis/specs/openapi/negative/mutations.py,sha256=
|
137
|
+
schemathesis/specs/openapi/negative/mutations.py,sha256=MIFVSWbZHW92KhpWruJT3XLisgc-rFnvYasRtwMmExs,19253
|
138
138
|
schemathesis/specs/openapi/negative/types.py,sha256=a7buCcVxNBG6ILBM3A7oNTAX0lyDseEtZndBuej8MbI,174
|
139
139
|
schemathesis/specs/openapi/negative/utils.py,sha256=ozcOIuASufLqZSgnKUACjX-EOZrrkuNdXX0SDnLoGYA,168
|
140
140
|
schemathesis/specs/openapi/stateful/__init__.py,sha256=0pu_iGjRiKuqUDN3ewz1zUOt6f1SdvSxVtHC5uK-CYw,14750
|
@@ -146,8 +146,8 @@ schemathesis/transport/prepare.py,sha256=qQ6zXBw5NN2AIM0bzLAc5Ryc3dmMb0R6xN14lnR
|
|
146
146
|
schemathesis/transport/requests.py,sha256=j5wI1Uo_PnVuP1eV8l6ddsXosyxAPQ1mLSyWEZmTI9I,8747
|
147
147
|
schemathesis/transport/serialization.py,sha256=jIMra1LqRGav0OX3Hx7mvORt38ll4cd2DKit2D58FN0,10531
|
148
148
|
schemathesis/transport/wsgi.py,sha256=RWSuUXPrl91GxAy8a4jyNNozOWVMRBxKx_tljlWA_Lo,5697
|
149
|
-
schemathesis-4.0.
|
150
|
-
schemathesis-4.0.
|
151
|
-
schemathesis-4.0.
|
152
|
-
schemathesis-4.0.
|
153
|
-
schemathesis-4.0.
|
149
|
+
schemathesis-4.0.0a8.dist-info/METADATA,sha256=hdJd_ASqgZ75dZ1EW27p3mL6CAG179nTb-cm1zVtQqk,10427
|
150
|
+
schemathesis-4.0.0a8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
151
|
+
schemathesis-4.0.0a8.dist-info/entry_points.txt,sha256=hiK3un-xfgPdwj9uj16YVDtTNpO128bmk0U82SMv8ZQ,152
|
152
|
+
schemathesis-4.0.0a8.dist-info/licenses/LICENSE,sha256=2Ve4J8v5jMQAWrT7r1nf3bI8Vflk3rZVQefiF2zpxwg,1121
|
153
|
+
schemathesis-4.0.0a8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|