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.
Files changed (34) hide show
  1. OpenApiDriver/openapi_executors.py +15 -11
  2. OpenApiDriver/openapi_reader.py +12 -13
  3. OpenApiDriver/openapidriver.libspec +5 -42
  4. OpenApiLibCore/__init__.py +0 -2
  5. OpenApiLibCore/annotations.py +8 -1
  6. OpenApiLibCore/data_generation/__init__.py +0 -2
  7. OpenApiLibCore/data_generation/body_data_generation.py +54 -73
  8. OpenApiLibCore/data_generation/data_generation_core.py +75 -82
  9. OpenApiLibCore/data_invalidation.py +38 -25
  10. OpenApiLibCore/dto_base.py +48 -105
  11. OpenApiLibCore/dto_utils.py +31 -3
  12. OpenApiLibCore/localized_faker.py +88 -0
  13. OpenApiLibCore/models.py +723 -0
  14. OpenApiLibCore/openapi_libcore.libspec +48 -284
  15. OpenApiLibCore/openapi_libcore.py +54 -71
  16. OpenApiLibCore/parameter_utils.py +20 -14
  17. OpenApiLibCore/path_functions.py +10 -10
  18. OpenApiLibCore/path_invalidation.py +5 -7
  19. OpenApiLibCore/protocols.py +13 -5
  20. OpenApiLibCore/request_data.py +67 -102
  21. OpenApiLibCore/resource_relations.py +6 -5
  22. OpenApiLibCore/validation.py +50 -167
  23. OpenApiLibCore/value_utils.py +46 -358
  24. openapi_libgen/__init__.py +0 -46
  25. openapi_libgen/command_line.py +7 -19
  26. openapi_libgen/generator.py +84 -0
  27. openapi_libgen/parsing_utils.py +9 -5
  28. openapi_libgen/spec_parser.py +41 -114
  29. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +2 -1
  30. robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
  31. robotframework_openapitools-1.0.0b3.dist-info/RECORD +0 -37
  32. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
  33. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
  34. {robotframework_openapitools-1.0.0b3.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
@@ -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, 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
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: Mapping[str, Any],
176
- current_value: Any,
177
- values_from_constraint: Iterable[Any] = tuple(),
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
- 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"]
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(value_schema=value_schema)
80
+ current_value = value_schema.get_valid_value()
194
81
 
195
- if (
196
- values_from_constraint
197
- and (
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
- 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
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
- if (
216
- invalid_value := get_value_out_of_bounds(
217
- value_schema=value_schema, current_value=current_value
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
- ) is not None:
220
- return invalid_value
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
- 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
112
+ invalid_values.append([{"invalid": [None, False]}, "null", None, True])
271
113
  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
- )
114
+ invalid_values.append(FAKE.uuid())
300
115
 
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
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 return None
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
- return None
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
- # None for empty string
420
- return invalid_value if invalid_value else None
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(values_from_constraint: list[str]) -> str: ...
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(values_from_constraint: list[bytes]) -> 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
@@ -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}"
@@ -1,9 +1,7 @@
1
1
  import argparse
2
2
  from pathlib import Path
3
3
 
4
- from prance import ResolvingParser
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
- parser = ResolvingParser(
39
- source,
40
- backend="openapi-spec-validator",
31
+ spec = generator.load_openapi_spec(
32
+ source=source,
41
33
  recursion_limit=args.recursion_limit,
42
- recursion_limit_handler=recursion_limit_handler,
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["info"]["title"]
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
- openapi_libgen.generate(
83
- openapi_spec=spec,
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
+ )
@@ -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
- for character in string_iterator:
12
- if character.isalpha():
13
- yield character
14
- break
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
- yield character
22
+ yield character.upper()
23
+ else:
24
+ yield character
21
25
 
22
26
  elif not capitalize_next_character:
23
27
  capitalize_next_character = True