robotframework-openapitools 1.0.0b3__py3-none-any.whl → 1.0.0b5__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/openapi_executors.py +15 -11
- OpenApiDriver/openapi_reader.py +12 -13
- OpenApiDriver/openapidriver.libspec +5 -42
- OpenApiLibCore/__init__.py +0 -2
- OpenApiLibCore/annotations.py +8 -1
- OpenApiLibCore/data_generation/__init__.py +0 -2
- OpenApiLibCore/data_generation/body_data_generation.py +54 -73
- OpenApiLibCore/data_generation/data_generation_core.py +75 -82
- OpenApiLibCore/data_invalidation.py +38 -25
- OpenApiLibCore/dto_base.py +48 -105
- OpenApiLibCore/dto_utils.py +31 -3
- OpenApiLibCore/localized_faker.py +88 -0
- OpenApiLibCore/models.py +723 -0
- OpenApiLibCore/openapi_libcore.libspec +48 -284
- OpenApiLibCore/openapi_libcore.py +54 -71
- OpenApiLibCore/parameter_utils.py +20 -14
- OpenApiLibCore/path_functions.py +10 -10
- OpenApiLibCore/path_invalidation.py +5 -7
- OpenApiLibCore/protocols.py +13 -5
- OpenApiLibCore/request_data.py +67 -102
- OpenApiLibCore/resource_relations.py +6 -5
- OpenApiLibCore/validation.py +50 -167
- OpenApiLibCore/value_utils.py +46 -358
- openapi_libgen/__init__.py +0 -46
- openapi_libgen/command_line.py +7 -19
- openapi_libgen/generator.py +84 -0
- openapi_libgen/parsing_utils.py +9 -5
- openapi_libgen/spec_parser.py +41 -114
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
- robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
- robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
- {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
OpenApiLibCore/value_utils.py
CHANGED
@@ -1,17 +1,13 @@
|
|
1
1
|
# mypy: disable-error-code=no-any-return
|
2
2
|
"""Utility module with functions to handle OpenAPI value types and restrictions."""
|
3
3
|
|
4
|
-
import base64
|
5
|
-
import datetime
|
6
4
|
from copy import deepcopy
|
7
|
-
from random import choice
|
8
|
-
from typing import Any,
|
9
|
-
|
10
|
-
import faker
|
11
|
-
import rstr
|
12
|
-
from robot.api import logger
|
5
|
+
from random import choice
|
6
|
+
from typing import Any, Iterable, cast, overload
|
13
7
|
|
14
8
|
from OpenApiLibCore.annotations import JSON
|
9
|
+
from OpenApiLibCore.localized_faker import FAKE
|
10
|
+
from OpenApiLibCore.models import ResolvedSchemaObjectTypes
|
15
11
|
|
16
12
|
|
17
13
|
class Ignore:
|
@@ -33,77 +29,6 @@ IGNORE = Ignore()
|
|
33
29
|
UNSET = UnSet()
|
34
30
|
|
35
31
|
|
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
32
|
def json_type_name_of_python_type(python_type: Any) -> str:
|
108
33
|
"""Return the JSON type name for supported Python types."""
|
109
34
|
if python_type == str:
|
@@ -142,215 +67,53 @@ def python_type_by_json_type_name(type_name: str) -> type:
|
|
142
67
|
raise ValueError(f"No Python type mapping for JSON type '{type_name}' available.")
|
143
68
|
|
144
69
|
|
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
70
|
def get_invalid_value(
|
175
|
-
value_schema:
|
176
|
-
current_value:
|
177
|
-
values_from_constraint: Iterable[
|
71
|
+
value_schema: ResolvedSchemaObjectTypes,
|
72
|
+
current_value: JSON,
|
73
|
+
values_from_constraint: Iterable[JSON] = tuple(),
|
178
74
|
) -> JSON | Ignore:
|
179
75
|
"""Return a random value that violates the provided value_schema."""
|
180
|
-
|
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"]
|
76
|
+
invalid_values: list[JSON | Ignore] = []
|
77
|
+
value_type = value_schema.type
|
191
78
|
|
192
79
|
if not isinstance(current_value, python_type_by_json_type_name(value_type)):
|
193
|
-
current_value = get_valid_value(
|
80
|
+
current_value = value_schema.get_valid_value()
|
194
81
|
|
195
|
-
if
|
196
|
-
|
197
|
-
|
198
|
-
invalid_value := get_invalid_value_from_constraint(
|
82
|
+
if values_from_constraint:
|
83
|
+
try:
|
84
|
+
return get_invalid_value_from_constraint(
|
199
85
|
values_from_constraint=list(values_from_constraint),
|
200
86
|
value_type=value_type,
|
201
87
|
)
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
) is not None:
|
213
|
-
return invalid_value
|
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
|
+
|
214
98
|
# Violate min / max values or length if possible
|
215
|
-
|
216
|
-
|
217
|
-
|
99
|
+
try:
|
100
|
+
values_out_of_bounds = value_schema.get_values_out_of_bounds(
|
101
|
+
current_value=current_value # type: ignore[arg-type]
|
218
102
|
)
|
219
|
-
|
220
|
-
|
103
|
+
invalid_values += values_out_of_bounds
|
104
|
+
except ValueError:
|
105
|
+
pass
|
106
|
+
|
221
107
|
# No value constraints or min / max ranges to violate, so change the data type
|
222
108
|
if value_type == "string":
|
223
109
|
# Since int / float / bool can always be cast to sting, change
|
224
110
|
# the string to a nested object.
|
225
111
|
# An array gets exploded in query strings, "null" is then often invalid
|
226
|
-
|
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
|
112
|
+
invalid_values.append([{"invalid": [None, False]}, "null", None, True])
|
271
113
|
else:
|
272
|
-
|
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
|
-
)
|
114
|
+
invalid_values.append(FAKE.uuid())
|
300
115
|
|
301
|
-
|
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
|
116
|
+
return choice(invalid_values)
|
354
117
|
|
355
118
|
|
356
119
|
def get_invalid_value_from_constraint(
|
@@ -368,12 +131,14 @@ def get_invalid_value_from_constraint(
|
|
368
131
|
# if the value is forced True or False, return the opposite to invalidate
|
369
132
|
if len(values_from_constraint) == 1 and value_type == "boolean":
|
370
133
|
return not values_from_constraint[0]
|
371
|
-
# for unsupported types or empty constraints lists
|
134
|
+
# for unsupported types or empty constraints lists raise a ValueError
|
372
135
|
if (
|
373
136
|
value_type not in ["string", "integer", "number", "array", "object"]
|
374
137
|
or not values_from_constraint
|
375
138
|
):
|
376
|
-
|
139
|
+
raise ValueError(
|
140
|
+
f"Cannot get invalid value for {value_type} from {values_from_constraint}"
|
141
|
+
)
|
377
142
|
|
378
143
|
values_from_constraint = deepcopy(values_from_constraint)
|
379
144
|
# for objects, keep the keys intact but update the values
|
@@ -416,8 +181,9 @@ def get_invalid_value_from_constraint(
|
|
416
181
|
|
417
182
|
str_or_bytes_list = cast(list[str] | list[bytes], values_from_constraint)
|
418
183
|
invalid_value = get_invalid_str_or_bytes(values_from_constraint=str_or_bytes_list)
|
419
|
-
|
420
|
-
|
184
|
+
if not invalid_value:
|
185
|
+
raise ValueError("Value invalidation yielded an empty string.")
|
186
|
+
return invalid_value
|
421
187
|
|
422
188
|
|
423
189
|
def get_invalid_int_or_number(values_from_constraint: list[int | float]) -> int | float:
|
@@ -431,11 +197,15 @@ def get_invalid_int_or_number(values_from_constraint: list[int | float]) -> int
|
|
431
197
|
|
432
198
|
|
433
199
|
@overload
|
434
|
-
def get_invalid_str_or_bytes(
|
200
|
+
def get_invalid_str_or_bytes(
|
201
|
+
values_from_constraint: list[str],
|
202
|
+
) -> str: ... # pragma: no cover
|
435
203
|
|
436
204
|
|
437
205
|
@overload
|
438
|
-
def get_invalid_str_or_bytes(
|
206
|
+
def get_invalid_str_or_bytes(
|
207
|
+
values_from_constraint: list[bytes],
|
208
|
+
) -> bytes: ... # pragma: no cover
|
439
209
|
|
440
210
|
|
441
211
|
def get_invalid_str_or_bytes(values_from_constraint: list[Any]) -> Any:
|
@@ -444,85 +214,3 @@ def get_invalid_str_or_bytes(values_from_constraint: list[Any]) -> Any:
|
|
444
214
|
for value in invalid_values:
|
445
215
|
invalid_value = invalid_value + value
|
446
216
|
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
|
openapi_libgen/__init__.py
CHANGED
@@ -1,46 +0,0 @@
|
|
1
|
-
from pathlib import Path
|
2
|
-
from typing import Any
|
3
|
-
|
4
|
-
from jinja2 import Environment, FileSystemLoader
|
5
|
-
|
6
|
-
from openapi_libgen.spec_parser import get_keyword_data
|
7
|
-
|
8
|
-
HERE = Path(__file__).parent.resolve()
|
9
|
-
INIT_TEMPLATE_PATH = HERE / "templates/__init__.jinja"
|
10
|
-
LIBRARY_TEMPLATE_PATH = HERE / "templates/library.jinja"
|
11
|
-
|
12
|
-
|
13
|
-
def generate(
|
14
|
-
openapi_spec: dict[str, Any],
|
15
|
-
output_folder: Path,
|
16
|
-
library_name: str,
|
17
|
-
module_name: str,
|
18
|
-
) -> str:
|
19
|
-
keyword_data = get_keyword_data(openapi_spec=openapi_spec)
|
20
|
-
|
21
|
-
library_folder = output_folder / library_name
|
22
|
-
library_folder.mkdir(parents=True, exist_ok=True)
|
23
|
-
|
24
|
-
environment = Environment(loader=FileSystemLoader(f"{HERE}/templates/"))
|
25
|
-
|
26
|
-
init_template = environment.get_template("__init__.jinja")
|
27
|
-
init_path = library_folder / "__init__.py"
|
28
|
-
init_content = init_template.render(
|
29
|
-
library_name=library_name,
|
30
|
-
module_name=module_name,
|
31
|
-
)
|
32
|
-
with open(init_path, mode="w", encoding="utf-8") as init_file:
|
33
|
-
init_file.write(init_content)
|
34
|
-
print(f"{init_path} created")
|
35
|
-
|
36
|
-
library_template = environment.get_template("library.jinja")
|
37
|
-
module_path = library_folder / f"{module_name}.py"
|
38
|
-
library_content = library_template.render(
|
39
|
-
library_name=library_name,
|
40
|
-
keywords=keyword_data,
|
41
|
-
)
|
42
|
-
with open(module_path, mode="w", encoding="utf-8") as library_file:
|
43
|
-
library_file.write(library_content)
|
44
|
-
print(f"{module_path} created")
|
45
|
-
|
46
|
-
return f"Generating {library_name} at {output_folder.resolve().as_posix()}/{module_name}"
|
openapi_libgen/command_line.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
import argparse
|
2
2
|
from pathlib import Path
|
3
3
|
|
4
|
-
from
|
5
|
-
|
6
|
-
import openapi_libgen
|
4
|
+
from openapi_libgen import generator
|
7
5
|
from openapi_libgen.parsing_utils import remove_unsafe_characters_from_string
|
8
6
|
|
9
7
|
parser = argparse.ArgumentParser(
|
@@ -27,24 +25,14 @@ def get_class_and_module_name_from_string(string: str) -> tuple[str, str]:
|
|
27
25
|
|
28
26
|
|
29
27
|
def main() -> None:
|
30
|
-
def recursion_limit_handler(
|
31
|
-
limit: int, refstring: str, recursions: object
|
32
|
-
) -> object: # pylint: disable=unused-argument
|
33
|
-
return args.recursion_default
|
34
|
-
|
35
28
|
if not (source := args.source):
|
36
29
|
source = input("Please provide a source for the generation: ")
|
37
30
|
|
38
|
-
|
39
|
-
source,
|
40
|
-
backend="openapi-spec-validator",
|
31
|
+
spec = generator.load_openapi_spec(
|
32
|
+
source=source,
|
41
33
|
recursion_limit=args.recursion_limit,
|
42
|
-
|
43
|
-
)
|
44
|
-
assert parser.specification is not None, (
|
45
|
-
"Source was loaded, but no specification was present after parsing."
|
34
|
+
recursion_default=args.recursion_default,
|
46
35
|
)
|
47
|
-
spec = parser.specification
|
48
36
|
|
49
37
|
if not (destination := args.destination):
|
50
38
|
destination = input(
|
@@ -57,7 +45,7 @@ def main() -> None:
|
|
57
45
|
args.name
|
58
46
|
)
|
59
47
|
else:
|
60
|
-
default_name = spec
|
48
|
+
default_name = spec.info.title
|
61
49
|
|
62
50
|
default_library_name, default_module_name = (
|
63
51
|
get_class_and_module_name_from_string(default_name)
|
@@ -79,8 +67,8 @@ def main() -> None:
|
|
79
67
|
default_module_name,
|
80
68
|
)
|
81
69
|
|
82
|
-
|
83
|
-
|
70
|
+
generator.generate(
|
71
|
+
openapi_object=spec,
|
84
72
|
output_folder=path,
|
85
73
|
library_name=safe_library_name,
|
86
74
|
module_name=safe_module_name,
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import sys
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from jinja2 import Environment, FileSystemLoader
|
5
|
+
from prance import ResolvingParser
|
6
|
+
|
7
|
+
from openapi_libgen.spec_parser import get_keyword_data
|
8
|
+
from OpenApiLibCore.models import OpenApiObject
|
9
|
+
|
10
|
+
HERE = Path(__file__).parent.resolve()
|
11
|
+
INIT_TEMPLATE_PATH = HERE / "templates/__init__.jinja"
|
12
|
+
LIBRARY_TEMPLATE_PATH = HERE / "templates/library.jinja"
|
13
|
+
|
14
|
+
|
15
|
+
def load_openapi_spec(
|
16
|
+
source: str, recursion_limit: int, recursion_default: object
|
17
|
+
) -> OpenApiObject:
|
18
|
+
def recursion_limit_handler(
|
19
|
+
limit: int, refstring: str, recursions: object
|
20
|
+
) -> object: # pylint: disable=unused-argument
|
21
|
+
return recursion_default # pragma: no cover
|
22
|
+
|
23
|
+
parser = ResolvingParser(
|
24
|
+
source,
|
25
|
+
backend="openapi-spec-validator",
|
26
|
+
recursion_limit=recursion_limit,
|
27
|
+
recursion_limit_handler=recursion_limit_handler,
|
28
|
+
)
|
29
|
+
assert parser.specification is not None, (
|
30
|
+
"Source was loaded, but no specification was present after parsing."
|
31
|
+
)
|
32
|
+
openapi_object = OpenApiObject.model_validate(parser.specification)
|
33
|
+
return openapi_object
|
34
|
+
|
35
|
+
|
36
|
+
def generate(
|
37
|
+
openapi_object: OpenApiObject,
|
38
|
+
output_folder: Path,
|
39
|
+
library_name: str,
|
40
|
+
module_name: str,
|
41
|
+
) -> str:
|
42
|
+
keyword_data = get_keyword_data(openapi_object=openapi_object)
|
43
|
+
|
44
|
+
library_folder = output_folder / library_name
|
45
|
+
library_folder.mkdir(parents=True, exist_ok=True)
|
46
|
+
|
47
|
+
environment = Environment(loader=FileSystemLoader(f"{HERE}/templates/"))
|
48
|
+
|
49
|
+
init_template = environment.get_template("__init__.jinja")
|
50
|
+
init_path = library_folder / "__init__.py"
|
51
|
+
init_content = init_template.render(
|
52
|
+
library_name=library_name,
|
53
|
+
module_name=module_name,
|
54
|
+
)
|
55
|
+
with open(init_path, mode="w", encoding="utf-8") as init_file:
|
56
|
+
init_file.write(init_content)
|
57
|
+
print(f"{init_path} created")
|
58
|
+
|
59
|
+
library_template = environment.get_template("library.jinja")
|
60
|
+
module_path = library_folder / f"{module_name}.py"
|
61
|
+
library_content = library_template.render(
|
62
|
+
library_name=library_name,
|
63
|
+
keywords=keyword_data,
|
64
|
+
)
|
65
|
+
with open(module_path, mode="w", encoding="utf-8") as library_file:
|
66
|
+
library_file.write(library_content)
|
67
|
+
print(f"{module_path} created")
|
68
|
+
|
69
|
+
return f"Generated {library_name} at {output_folder.resolve().as_posix()}/{module_name}"
|
70
|
+
|
71
|
+
|
72
|
+
if __name__ == "__main__": # pragma: no cover
|
73
|
+
source = sys.argv[1]
|
74
|
+
destination = Path(sys.argv[2])
|
75
|
+
library_name = sys.argv[3]
|
76
|
+
module_name = sys.argv[4]
|
77
|
+
spec = load_openapi_spec(source=source, recursion_limit=1, recursion_default={})
|
78
|
+
|
79
|
+
result_string = generate(
|
80
|
+
openapi_object=spec,
|
81
|
+
output_folder=destination,
|
82
|
+
library_name=library_name,
|
83
|
+
module_name=module_name,
|
84
|
+
)
|
openapi_libgen/parsing_utils.py
CHANGED
@@ -8,16 +8,20 @@ def remove_unsafe_characters_from_string(string: str) -> str:
|
|
8
8
|
string_iterator = iter(string)
|
9
9
|
capitalize_next_character = False
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
# The first character must be A-z or _
|
12
|
+
first_character = next(string_iterator, "_")
|
13
|
+
if first_character.isalpha() or first_character == "_":
|
14
|
+
yield first_character
|
15
|
+
elif first_character.isnumeric():
|
16
|
+
yield "_" + first_character
|
15
17
|
|
16
18
|
for character in string_iterator:
|
17
19
|
if character.isalnum():
|
18
20
|
if capitalize_next_character:
|
19
21
|
capitalize_next_character = False
|
20
|
-
|
22
|
+
yield character.upper()
|
23
|
+
else:
|
24
|
+
yield character
|
21
25
|
|
22
26
|
elif not capitalize_next_character:
|
23
27
|
capitalize_next_character = True
|