robotframework-openapitools 0.3.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

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