robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0b1__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.
- OpenApiDriver/__init__.py +44 -41
- OpenApiDriver/openapi_executors.py +48 -42
- OpenApiDriver/openapi_reader.py +115 -116
- OpenApiDriver/openapidriver.libspec +72 -62
- OpenApiDriver/openapidriver.py +25 -19
- OpenApiLibCore/__init__.py +13 -11
- OpenApiLibCore/annotations.py +3 -0
- OpenApiLibCore/data_generation/__init__.py +12 -0
- OpenApiLibCore/data_generation/body_data_generation.py +269 -0
- OpenApiLibCore/data_generation/data_generation_core.py +240 -0
- OpenApiLibCore/data_invalidation.py +281 -0
- OpenApiLibCore/dto_base.py +43 -40
- OpenApiLibCore/dto_utils.py +97 -85
- OpenApiLibCore/oas_cache.py +14 -13
- OpenApiLibCore/openapi_libcore.libspec +361 -188
- OpenApiLibCore/openapi_libcore.py +392 -1645
- OpenApiLibCore/parameter_utils.py +89 -0
- OpenApiLibCore/path_functions.py +215 -0
- OpenApiLibCore/path_invalidation.py +44 -0
- OpenApiLibCore/protocols.py +30 -0
- OpenApiLibCore/request_data.py +275 -0
- OpenApiLibCore/resource_relations.py +54 -0
- OpenApiLibCore/validation.py +497 -0
- OpenApiLibCore/value_utils.py +528 -481
- openapi_libgen/__init__.py +46 -0
- openapi_libgen/command_line.py +87 -0
- openapi_libgen/parsing_utils.py +26 -0
- openapi_libgen/spec_parser.py +212 -0
- openapi_libgen/templates/__init__.jinja +3 -0
- openapi_libgen/templates/library.jinja +30 -0
- robotframework_openapitools-1.0.0b1.dist-info/METADATA +237 -0
- robotframework_openapitools-1.0.0b1.dist-info/RECORD +37 -0
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/WHEEL +1 -1
- robotframework_openapitools-1.0.0b1.dist-info/entry_points.txt +3 -0
- roboswag/__init__.py +0 -9
- roboswag/__main__.py +0 -3
- roboswag/auth.py +0 -44
- roboswag/cli.py +0 -80
- roboswag/core.py +0 -85
- roboswag/generate/__init__.py +0 -1
- roboswag/generate/generate.py +0 -121
- roboswag/generate/models/__init__.py +0 -0
- roboswag/generate/models/api.py +0 -219
- roboswag/generate/models/definition.py +0 -28
- roboswag/generate/models/endpoint.py +0 -68
- roboswag/generate/models/parameter.py +0 -25
- roboswag/generate/models/response.py +0 -8
- roboswag/generate/models/tag.py +0 -16
- roboswag/generate/models/utils.py +0 -60
- roboswag/generate/templates/api_init.jinja +0 -15
- roboswag/generate/templates/models.jinja +0 -7
- roboswag/generate/templates/paths.jinja +0 -68
- roboswag/logger.py +0 -33
- roboswag/validate/__init__.py +0 -6
- roboswag/validate/core.py +0 -3
- roboswag/validate/schema.py +0 -21
- roboswag/validate/text_response.py +0 -14
- robotframework_openapitools-0.3.0.dist-info/METADATA +0 -41
- robotframework_openapitools-0.3.0.dist-info/RECORD +0 -41
- {robotframework_openapitools-0.3.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/LICENSE +0 -0
OpenApiLibCore/value_utils.py
CHANGED
@@ -1,481 +1,528 @@
|
|
1
|
-
# mypy: disable-error-code=no-any-return
|
2
|
-
"""Utility module with functions to handle OpenAPI value types and restrictions."""
|
3
|
-
|
4
|
-
import
|
5
|
-
|
6
|
-
from
|
7
|
-
from random import choice, randint, uniform
|
8
|
-
from typing import Any, Callable,
|
9
|
-
|
10
|
-
import faker
|
11
|
-
import rstr
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
@property
|
48
|
-
def
|
49
|
-
return self.fake.
|
50
|
-
|
51
|
-
@property
|
52
|
-
def
|
53
|
-
return self.fake.
|
54
|
-
|
55
|
-
@property
|
56
|
-
def
|
57
|
-
return self.fake.
|
58
|
-
|
59
|
-
@property
|
60
|
-
def
|
61
|
-
return self.fake.
|
62
|
-
|
63
|
-
@property
|
64
|
-
def
|
65
|
-
return self.fake.
|
66
|
-
|
67
|
-
@property
|
68
|
-
def
|
69
|
-
return self.fake.
|
70
|
-
|
71
|
-
@property
|
72
|
-
def
|
73
|
-
return self.fake.
|
74
|
-
|
75
|
-
@property
|
76
|
-
def
|
77
|
-
return self.fake.
|
78
|
-
|
79
|
-
@property
|
80
|
-
def
|
81
|
-
return self.fake.
|
82
|
-
|
83
|
-
@property
|
84
|
-
def
|
85
|
-
return self.fake.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
if
|
137
|
-
return
|
138
|
-
if
|
139
|
-
return
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
if
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
)
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
)
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
if
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
else:
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
if
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
if
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
"""
|
317
|
-
|
318
|
-
""
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
value
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
if
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
""
|
346
|
-
|
347
|
-
|
348
|
-
""
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
#
|
366
|
-
if
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
return
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
if
|
466
|
-
invalid_value
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
#
|
475
|
-
if
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
1
|
+
# mypy: disable-error-code=no-any-return
|
2
|
+
"""Utility module with functions to handle OpenAPI value types and restrictions."""
|
3
|
+
|
4
|
+
import base64
|
5
|
+
import datetime
|
6
|
+
from copy import deepcopy
|
7
|
+
from random import choice, randint, uniform
|
8
|
+
from typing import Any, Callable, Iterable, Mapping, cast, overload
|
9
|
+
|
10
|
+
import faker
|
11
|
+
import rstr
|
12
|
+
from robot.api import logger
|
13
|
+
|
14
|
+
from OpenApiLibCore.annotations import JSON
|
15
|
+
|
16
|
+
|
17
|
+
class Ignore:
|
18
|
+
"""Helper class to flag properties to be ignored in data generation."""
|
19
|
+
|
20
|
+
def __str__(self) -> str:
|
21
|
+
return "IGNORE"
|
22
|
+
|
23
|
+
|
24
|
+
class UnSet:
|
25
|
+
"""Helper class to flag arguments that have not been set in a keyword call."""
|
26
|
+
|
27
|
+
def __str__(self) -> str:
|
28
|
+
return "UNSET"
|
29
|
+
|
30
|
+
|
31
|
+
IGNORE = Ignore()
|
32
|
+
|
33
|
+
UNSET = UnSet()
|
34
|
+
|
35
|
+
|
36
|
+
class LocalizedFaker:
|
37
|
+
"""Class to support setting a locale post-init."""
|
38
|
+
|
39
|
+
# pylint: disable=missing-function-docstring
|
40
|
+
def __init__(self) -> None:
|
41
|
+
self.fake = faker.Faker()
|
42
|
+
|
43
|
+
def set_locale(self, locale: str | list[str]) -> None:
|
44
|
+
"""Update the fake attribute with a Faker instance with the provided locale."""
|
45
|
+
self.fake = faker.Faker(locale)
|
46
|
+
|
47
|
+
@property
|
48
|
+
def date(self) -> Callable[[], str]:
|
49
|
+
return self.fake.date
|
50
|
+
|
51
|
+
@property
|
52
|
+
def date_time(self) -> Callable[[], datetime.datetime]:
|
53
|
+
return self.fake.date_time
|
54
|
+
|
55
|
+
@property
|
56
|
+
def password(self) -> Callable[[], str]:
|
57
|
+
return self.fake.password
|
58
|
+
|
59
|
+
@property
|
60
|
+
def binary(self) -> Callable[[], bytes]:
|
61
|
+
return self.fake.binary
|
62
|
+
|
63
|
+
@property
|
64
|
+
def email(self) -> Callable[[], str]:
|
65
|
+
return self.fake.safe_email
|
66
|
+
|
67
|
+
@property
|
68
|
+
def uuid(self) -> Callable[[], str]:
|
69
|
+
return self.fake.uuid4
|
70
|
+
|
71
|
+
@property
|
72
|
+
def uri(self) -> Callable[[], str]:
|
73
|
+
return self.fake.uri
|
74
|
+
|
75
|
+
@property
|
76
|
+
def url(self) -> Callable[[], str]:
|
77
|
+
return self.fake.url
|
78
|
+
|
79
|
+
@property
|
80
|
+
def hostname(self) -> Callable[[], str]:
|
81
|
+
return self.fake.hostname
|
82
|
+
|
83
|
+
@property
|
84
|
+
def ipv4(self) -> Callable[[], str]:
|
85
|
+
return self.fake.ipv4
|
86
|
+
|
87
|
+
@property
|
88
|
+
def ipv6(self) -> Callable[[], str]:
|
89
|
+
return self.fake.ipv6
|
90
|
+
|
91
|
+
@property
|
92
|
+
def name(self) -> Callable[[], str]:
|
93
|
+
return self.fake.name
|
94
|
+
|
95
|
+
@property
|
96
|
+
def text(self) -> Callable[[], str]:
|
97
|
+
return self.fake.text
|
98
|
+
|
99
|
+
@property
|
100
|
+
def description(self) -> Callable[[], str]:
|
101
|
+
return self.fake.text
|
102
|
+
|
103
|
+
|
104
|
+
FAKE = LocalizedFaker()
|
105
|
+
|
106
|
+
|
107
|
+
def json_type_name_of_python_type(python_type: Any) -> str:
|
108
|
+
"""Return the JSON type name for supported Python types."""
|
109
|
+
if python_type == str:
|
110
|
+
return "string"
|
111
|
+
if python_type == bool:
|
112
|
+
return "boolean"
|
113
|
+
if python_type == int:
|
114
|
+
return "integer"
|
115
|
+
if python_type == float:
|
116
|
+
return "number"
|
117
|
+
if python_type == list:
|
118
|
+
return "array"
|
119
|
+
if python_type == dict:
|
120
|
+
return "object"
|
121
|
+
if python_type == type(None):
|
122
|
+
return "null"
|
123
|
+
raise ValueError(f"No json type mapping for Python type {python_type} available.")
|
124
|
+
|
125
|
+
|
126
|
+
def python_type_by_json_type_name(type_name: str) -> type:
|
127
|
+
"""Return the Python type based on the JSON type name."""
|
128
|
+
if type_name == "string":
|
129
|
+
return str
|
130
|
+
if type_name == "boolean":
|
131
|
+
return bool
|
132
|
+
if type_name == "integer":
|
133
|
+
return int
|
134
|
+
if type_name == "number":
|
135
|
+
return float
|
136
|
+
if type_name == "array":
|
137
|
+
return list
|
138
|
+
if type_name == "object":
|
139
|
+
return dict
|
140
|
+
if type_name == "null":
|
141
|
+
return type(None)
|
142
|
+
raise ValueError(f"No Python type mapping for JSON type '{type_name}' available.")
|
143
|
+
|
144
|
+
|
145
|
+
def get_valid_value(value_schema: Mapping[str, Any]) -> JSON:
|
146
|
+
"""Return a random value that is valid under the provided value_schema."""
|
147
|
+
value_schema = deepcopy(value_schema)
|
148
|
+
|
149
|
+
if value_schema.get("types"):
|
150
|
+
value_schema = choice(value_schema["types"])
|
151
|
+
|
152
|
+
if (from_const := value_schema.get("const")) is not None:
|
153
|
+
return from_const
|
154
|
+
if from_enum := value_schema.get("enum"):
|
155
|
+
return choice(from_enum)
|
156
|
+
|
157
|
+
value_type = value_schema["type"]
|
158
|
+
|
159
|
+
if value_type == "null":
|
160
|
+
return None
|
161
|
+
if value_type == "boolean":
|
162
|
+
return FAKE.fake.boolean()
|
163
|
+
if value_type == "integer":
|
164
|
+
return get_random_int(value_schema=value_schema)
|
165
|
+
if value_type == "number":
|
166
|
+
return get_random_float(value_schema=value_schema)
|
167
|
+
if value_type == "string":
|
168
|
+
return get_random_string(value_schema=value_schema)
|
169
|
+
if value_type == "array":
|
170
|
+
return get_random_array(value_schema=value_schema)
|
171
|
+
raise NotImplementedError(f"Type '{value_type}' is currently not supported")
|
172
|
+
|
173
|
+
|
174
|
+
def get_invalid_value(
|
175
|
+
value_schema: Mapping[str, Any],
|
176
|
+
current_value: Any,
|
177
|
+
values_from_constraint: Iterable[Any] = tuple(),
|
178
|
+
) -> JSON | Ignore:
|
179
|
+
"""Return a random value that violates the provided value_schema."""
|
180
|
+
value_schema = deepcopy(value_schema)
|
181
|
+
|
182
|
+
if value_schemas := value_schema.get("types"):
|
183
|
+
if len(value_schemas) > 1:
|
184
|
+
value_schemas = [
|
185
|
+
schema for schema in value_schemas if schema["type"] != "null"
|
186
|
+
]
|
187
|
+
value_schema = choice(value_schemas)
|
188
|
+
|
189
|
+
invalid_value: Any = None
|
190
|
+
value_type = value_schema["type"]
|
191
|
+
|
192
|
+
if not isinstance(current_value, python_type_by_json_type_name(value_type)):
|
193
|
+
current_value = get_valid_value(value_schema=value_schema)
|
194
|
+
|
195
|
+
if (
|
196
|
+
values_from_constraint
|
197
|
+
and (
|
198
|
+
invalid_value := get_invalid_value_from_constraint(
|
199
|
+
values_from_constraint=list(values_from_constraint),
|
200
|
+
value_type=value_type,
|
201
|
+
)
|
202
|
+
)
|
203
|
+
is not None
|
204
|
+
):
|
205
|
+
return invalid_value
|
206
|
+
# If an enum is possible, combine the values from the enum to invalidate the value
|
207
|
+
if enum_values := value_schema.get("enum"):
|
208
|
+
if (
|
209
|
+
invalid_value := get_invalid_value_from_enum(
|
210
|
+
values=enum_values, value_type=value_type
|
211
|
+
)
|
212
|
+
) is not None:
|
213
|
+
return invalid_value
|
214
|
+
# Violate min / max values or length if possible
|
215
|
+
if (
|
216
|
+
invalid_value := get_value_out_of_bounds(
|
217
|
+
value_schema=value_schema, current_value=current_value
|
218
|
+
)
|
219
|
+
) is not None:
|
220
|
+
return invalid_value
|
221
|
+
# No value constraints or min / max ranges to violate, so change the data type
|
222
|
+
if value_type == "string":
|
223
|
+
# Since int / float / bool can always be cast to sting, change
|
224
|
+
# the string to a nested object.
|
225
|
+
# An array gets exploded in query strings, "null" is then often invalid
|
226
|
+
return [{"invalid": [None, False]}, "null", None, True]
|
227
|
+
logger.debug(f"property type changed from {value_type} to random string")
|
228
|
+
return FAKE.uuid()
|
229
|
+
|
230
|
+
|
231
|
+
def get_random_int(value_schema: Mapping[str, Any]) -> int:
|
232
|
+
"""Generate a random int within the min/max range of the schema, if specified."""
|
233
|
+
# Use int32 integers if "format" does not specify int64
|
234
|
+
property_format = value_schema.get("format", "int32")
|
235
|
+
if property_format == "int64":
|
236
|
+
min_int = -9223372036854775808
|
237
|
+
max_int = 9223372036854775807
|
238
|
+
else:
|
239
|
+
min_int = -2147483648
|
240
|
+
max_int = 2147483647
|
241
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
242
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer
|
243
|
+
minimum = value_schema.get("minimum", min_int)
|
244
|
+
maximum = value_schema.get("maximum", max_int)
|
245
|
+
if (exclusive_minimum := value_schema.get("exclusiveMinimum")) is not None:
|
246
|
+
if isinstance(exclusive_minimum, bool):
|
247
|
+
if exclusive_minimum:
|
248
|
+
minimum += 1
|
249
|
+
else:
|
250
|
+
minimum = exclusive_minimum + 1
|
251
|
+
if (exclusive_maximum := value_schema.get("exclusiveMaximum")) is not None:
|
252
|
+
if isinstance(exclusive_maximum, bool):
|
253
|
+
if exclusive_maximum:
|
254
|
+
maximum -= 1
|
255
|
+
else:
|
256
|
+
maximum = exclusive_maximum - 1
|
257
|
+
return randint(minimum, maximum)
|
258
|
+
|
259
|
+
|
260
|
+
def get_random_float(value_schema: Mapping[str, Any]) -> float:
|
261
|
+
"""Generate a random float within the min/max range of the schema, if specified."""
|
262
|
+
# Python floats are already double precision, so no check for "format"
|
263
|
+
minimum = value_schema.get("minimum")
|
264
|
+
maximum = value_schema.get("maximum")
|
265
|
+
if minimum is None:
|
266
|
+
if maximum is None:
|
267
|
+
minimum = -1.0
|
268
|
+
maximum = 1.0
|
269
|
+
else:
|
270
|
+
minimum = maximum - 1.0
|
271
|
+
else:
|
272
|
+
if maximum is None:
|
273
|
+
maximum = minimum + 1.0
|
274
|
+
if maximum < minimum:
|
275
|
+
raise ValueError(f"maximum of {maximum} is less than minimum of {minimum}")
|
276
|
+
|
277
|
+
# For simplicity's sake, exclude both boundaries if one boundary is exclusive
|
278
|
+
exclude_boundaries = False
|
279
|
+
|
280
|
+
exclusive_minimum = value_schema.get("exclusiveMinimum", False)
|
281
|
+
exclusive_maximum = value_schema.get("exclusiveMaximum", False)
|
282
|
+
# OAS 3.0: exclusiveMinimum/Maximum is a bool in combination with minimum/maximum
|
283
|
+
# OAS 3.1: exclusiveMinimum/Maximum is an integer or number
|
284
|
+
if not isinstance(exclusive_minimum, bool):
|
285
|
+
exclude_boundaries = True
|
286
|
+
minimum = exclusive_minimum
|
287
|
+
else:
|
288
|
+
exclude_boundaries = exclusive_minimum
|
289
|
+
if not isinstance(exclusive_maximum, bool):
|
290
|
+
exclude_boundaries = True
|
291
|
+
maximum = exclusive_maximum
|
292
|
+
else:
|
293
|
+
exclude_boundaries = exclusive_minimum or exclusive_maximum
|
294
|
+
|
295
|
+
if exclude_boundaries and minimum == maximum:
|
296
|
+
raise ValueError(
|
297
|
+
f"maximum of {maximum} is equal to minimum of {minimum} and "
|
298
|
+
f"exclusiveMinimum or exclusiveMaximum is specified"
|
299
|
+
)
|
300
|
+
|
301
|
+
while exclude_boundaries:
|
302
|
+
result = uniform(minimum, maximum)
|
303
|
+
if minimum < result < maximum: # pragma: no cover
|
304
|
+
return result
|
305
|
+
return uniform(minimum, maximum)
|
306
|
+
|
307
|
+
|
308
|
+
def get_random_string(value_schema: Mapping[str, Any]) -> bytes | str:
|
309
|
+
"""Generate a random string within the min/max length in the schema, if specified."""
|
310
|
+
# if a pattern is provided, format and min/max length can be ignored
|
311
|
+
if pattern := value_schema.get("pattern"):
|
312
|
+
return rstr.xeger(pattern)
|
313
|
+
minimum = value_schema.get("minLength", 0)
|
314
|
+
maximum = value_schema.get("maxLength", 36)
|
315
|
+
maximum = max(minimum, maximum)
|
316
|
+
format_ = value_schema.get("format", "uuid")
|
317
|
+
# byte is a special case due to the required encoding
|
318
|
+
if format_ == "byte":
|
319
|
+
data = FAKE.uuid()
|
320
|
+
return base64.b64encode(data.encode("utf-8"))
|
321
|
+
value = fake_string(string_format=format_)
|
322
|
+
while len(value) < minimum:
|
323
|
+
# use fake.name() to ensure the returned string uses the provided locale
|
324
|
+
value = value + FAKE.name()
|
325
|
+
if len(value) > maximum:
|
326
|
+
value = value[:maximum]
|
327
|
+
return value
|
328
|
+
|
329
|
+
|
330
|
+
def fake_string(string_format: str) -> str:
|
331
|
+
"""
|
332
|
+
Generate a random string based on the provided format if the format is supported.
|
333
|
+
"""
|
334
|
+
# format names may contain -, which is invalid in Python naming
|
335
|
+
string_format = string_format.replace("-", "_")
|
336
|
+
fake_generator = getattr(FAKE, string_format, FAKE.uuid)
|
337
|
+
value: str = fake_generator()
|
338
|
+
if isinstance(value, datetime.datetime):
|
339
|
+
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
|
340
|
+
return value
|
341
|
+
|
342
|
+
|
343
|
+
def get_random_array(value_schema: Mapping[str, Any]) -> list[JSON]:
|
344
|
+
"""Generate a list with random elements as specified by the schema."""
|
345
|
+
minimum = value_schema.get("minItems", 0)
|
346
|
+
maximum = value_schema.get("maxItems", 1)
|
347
|
+
maximum = max(minimum, maximum)
|
348
|
+
items_schema = value_schema["items"]
|
349
|
+
value = []
|
350
|
+
for _ in range(maximum):
|
351
|
+
item_value = get_valid_value(items_schema)
|
352
|
+
value.append(item_value)
|
353
|
+
return value
|
354
|
+
|
355
|
+
|
356
|
+
def get_invalid_value_from_constraint(
|
357
|
+
values_from_constraint: list[JSON | Ignore], value_type: str
|
358
|
+
) -> JSON | Ignore:
|
359
|
+
"""
|
360
|
+
Return a value of the same type as the values in the values_from_constraints that
|
361
|
+
is not in the values_from_constraints, if possible. Otherwise returns None.
|
362
|
+
"""
|
363
|
+
# if IGNORE is in the values_from_constraints, the parameter needs to be
|
364
|
+
# ignored for an OK response so leaving the value at it's original value
|
365
|
+
# should result in the specified error response
|
366
|
+
if any(map(lambda x: isinstance(x, Ignore), values_from_constraint)):
|
367
|
+
return IGNORE
|
368
|
+
# if the value is forced True or False, return the opposite to invalidate
|
369
|
+
if len(values_from_constraint) == 1 and value_type == "boolean":
|
370
|
+
return not values_from_constraint[0]
|
371
|
+
# for unsupported types or empty constraints lists return None
|
372
|
+
if (
|
373
|
+
value_type not in ["string", "integer", "number", "array", "object"]
|
374
|
+
or not values_from_constraint
|
375
|
+
):
|
376
|
+
return None
|
377
|
+
|
378
|
+
values_from_constraint = deepcopy(values_from_constraint)
|
379
|
+
# for objects, keep the keys intact but update the values
|
380
|
+
if value_type == "object":
|
381
|
+
valid_object = cast(dict[str, JSON], values_from_constraint.pop())
|
382
|
+
invalid_object: dict[str, JSON] = {}
|
383
|
+
for key, value in valid_object.items():
|
384
|
+
python_type_of_value = type(value)
|
385
|
+
json_type_of_value = json_type_name_of_python_type(python_type_of_value)
|
386
|
+
invalid_value = cast(
|
387
|
+
JSON,
|
388
|
+
get_invalid_value_from_constraint(
|
389
|
+
values_from_constraint=[value],
|
390
|
+
value_type=json_type_of_value,
|
391
|
+
),
|
392
|
+
)
|
393
|
+
invalid_object[key] = invalid_value
|
394
|
+
return invalid_object
|
395
|
+
|
396
|
+
# for arrays, update each value in the array to a value of the same type
|
397
|
+
if value_type == "array":
|
398
|
+
valid_array = cast(list[JSON], values_from_constraint.pop())
|
399
|
+
invalid_array: list[JSON] = []
|
400
|
+
for value in valid_array:
|
401
|
+
python_type_of_value = type(value)
|
402
|
+
json_type_of_value = json_type_name_of_python_type(python_type_of_value)
|
403
|
+
invalid_value = cast(
|
404
|
+
JSON,
|
405
|
+
get_invalid_value_from_constraint(
|
406
|
+
values_from_constraint=[value],
|
407
|
+
value_type=json_type_of_value,
|
408
|
+
),
|
409
|
+
)
|
410
|
+
invalid_array.append(invalid_value)
|
411
|
+
return invalid_array
|
412
|
+
|
413
|
+
if value_type in ["integer", "number"]:
|
414
|
+
int_or_number_list = cast(list[int | float], values_from_constraint)
|
415
|
+
return get_invalid_int_or_number(values_from_constraint=int_or_number_list)
|
416
|
+
|
417
|
+
str_or_bytes_list = cast(list[str] | list[bytes], values_from_constraint)
|
418
|
+
invalid_value = get_invalid_str_or_bytes(values_from_constraint=str_or_bytes_list)
|
419
|
+
# None for empty string
|
420
|
+
return invalid_value if invalid_value else None
|
421
|
+
|
422
|
+
|
423
|
+
def get_invalid_int_or_number(values_from_constraint: list[int | float]) -> int | float:
|
424
|
+
invalid_values = 2 * values_from_constraint
|
425
|
+
invalid_value = invalid_values.pop()
|
426
|
+
for value in invalid_values:
|
427
|
+
invalid_value = abs(invalid_value) + abs(value)
|
428
|
+
if not invalid_value:
|
429
|
+
invalid_value += 1
|
430
|
+
return invalid_value
|
431
|
+
|
432
|
+
|
433
|
+
@overload
|
434
|
+
def get_invalid_str_or_bytes(values_from_constraint: list[str]) -> str: ...
|
435
|
+
|
436
|
+
|
437
|
+
@overload
|
438
|
+
def get_invalid_str_or_bytes(values_from_constraint: list[bytes]) -> bytes: ...
|
439
|
+
|
440
|
+
|
441
|
+
def get_invalid_str_or_bytes(values_from_constraint: list[Any]) -> Any:
|
442
|
+
invalid_values = 2 * values_from_constraint
|
443
|
+
invalid_value = invalid_values.pop()
|
444
|
+
for value in invalid_values:
|
445
|
+
invalid_value = invalid_value + value
|
446
|
+
return invalid_value
|
447
|
+
|
448
|
+
|
449
|
+
def get_invalid_value_from_enum(values: list[Any], value_type: str) -> JSON:
|
450
|
+
"""Return a value not in the enum by combining the enum values."""
|
451
|
+
if value_type == "string":
|
452
|
+
invalid_value: Any = ""
|
453
|
+
elif value_type in ["integer", "number"]:
|
454
|
+
invalid_value = 0
|
455
|
+
elif value_type == "array":
|
456
|
+
invalid_value = []
|
457
|
+
elif value_type == "object":
|
458
|
+
# force creation of a new object since we will be modifying it
|
459
|
+
invalid_value = {**values[0]}
|
460
|
+
else:
|
461
|
+
logger.warn(f"Cannot invalidate enum value with type {value_type}")
|
462
|
+
return None
|
463
|
+
for value in values:
|
464
|
+
# repeat each addition to ensure single-item enums are invalidated
|
465
|
+
if value_type in ["integer", "number"]:
|
466
|
+
invalid_value += abs(value) + abs(value)
|
467
|
+
if value_type == "string":
|
468
|
+
invalid_value += value + value
|
469
|
+
if value_type == "array":
|
470
|
+
invalid_value.extend(value)
|
471
|
+
invalid_value.extend(value)
|
472
|
+
# objects are a special case, since they must be of the same type / class
|
473
|
+
# invalid_value.update(value) will end up with the last value in the list,
|
474
|
+
# which is a valid value, so another approach is needed
|
475
|
+
if value_type == "object":
|
476
|
+
for key in invalid_value.keys():
|
477
|
+
invalid_value[key] = value.get(key)
|
478
|
+
if invalid_value not in values:
|
479
|
+
return invalid_value
|
480
|
+
return invalid_value
|
481
|
+
|
482
|
+
|
483
|
+
def get_value_out_of_bounds(
|
484
|
+
value_schema: Mapping[str, Any], current_value: JSON
|
485
|
+
) -> JSON:
|
486
|
+
"""
|
487
|
+
Return a value just outside the value or length range if specified in the
|
488
|
+
provided schema, otherwise None is returned.
|
489
|
+
"""
|
490
|
+
value_type = value_schema["type"]
|
491
|
+
|
492
|
+
if value_type in ["integer", "number"]:
|
493
|
+
if (minimum := value_schema.get("minimum")) is not None:
|
494
|
+
if value_schema.get("exclusiveMinimum") is True:
|
495
|
+
return minimum
|
496
|
+
return minimum - 1
|
497
|
+
if (maximum := value_schema.get("maximum")) is not None:
|
498
|
+
if value_schema.get("exclusiveMaximum") is True:
|
499
|
+
return maximum
|
500
|
+
return maximum + 1
|
501
|
+
# In OAS 3.1 exclusveMinimum/Maximum are no longer boolean but instead integer
|
502
|
+
# or number and minimum/maximum should not be used with exclusiveMinimum/Maximum
|
503
|
+
if (exclusive_minimum := value_schema.get("exclusiveMinimum")) is not None:
|
504
|
+
return exclusive_minimum
|
505
|
+
if (exclusive_maximum := value_schema.get("exclusiveMaximum")) is not None:
|
506
|
+
return exclusive_maximum
|
507
|
+
if value_type == "array":
|
508
|
+
current_list = cast(list[JSON], current_value)
|
509
|
+
if minimum := value_schema.get("minItems", 0) > 0:
|
510
|
+
return current_list[0 : minimum - 1]
|
511
|
+
if (maximum := value_schema.get("maxItems")) is not None:
|
512
|
+
invalid_value = current_list if current_list else ["x"]
|
513
|
+
while len(invalid_value) <= maximum:
|
514
|
+
invalid_value.append(choice(invalid_value)) # pyright: ignore[reportArgumentType]
|
515
|
+
return invalid_value # type: ignore[unused-ignore]
|
516
|
+
if value_type == "string":
|
517
|
+
current_string = cast(str, current_value)
|
518
|
+
# if there is a minimum length, send 1 character less
|
519
|
+
if minimum := value_schema.get("minLength", 0):
|
520
|
+
return current_string[0 : minimum - 1]
|
521
|
+
# if there is a maximum length, send 1 character more
|
522
|
+
if maximum := value_schema.get("maxLength"):
|
523
|
+
invalid_string_value = current_string if current_string else "x"
|
524
|
+
# add random characters from the current value to prevent adding new characters
|
525
|
+
while len(invalid_string_value) <= maximum:
|
526
|
+
invalid_string_value += choice(invalid_string_value)
|
527
|
+
return invalid_string_value
|
528
|
+
return None
|