robotframework-openapitools 0.4.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.
Files changed (60) hide show
  1. OpenApiDriver/__init__.py +44 -41
  2. OpenApiDriver/openapi_executors.py +40 -39
  3. OpenApiDriver/openapi_reader.py +115 -116
  4. OpenApiDriver/openapidriver.libspec +71 -61
  5. OpenApiDriver/openapidriver.py +25 -19
  6. OpenApiLibCore/__init__.py +13 -11
  7. OpenApiLibCore/annotations.py +3 -0
  8. OpenApiLibCore/data_generation/__init__.py +12 -0
  9. OpenApiLibCore/data_generation/body_data_generation.py +269 -0
  10. OpenApiLibCore/data_generation/data_generation_core.py +240 -0
  11. OpenApiLibCore/data_invalidation.py +281 -0
  12. OpenApiLibCore/dto_base.py +29 -35
  13. OpenApiLibCore/dto_utils.py +97 -85
  14. OpenApiLibCore/oas_cache.py +14 -13
  15. OpenApiLibCore/openapi_libcore.libspec +350 -193
  16. OpenApiLibCore/openapi_libcore.py +392 -1698
  17. OpenApiLibCore/parameter_utils.py +89 -0
  18. OpenApiLibCore/path_functions.py +215 -0
  19. OpenApiLibCore/path_invalidation.py +44 -0
  20. OpenApiLibCore/protocols.py +30 -0
  21. OpenApiLibCore/request_data.py +275 -0
  22. OpenApiLibCore/resource_relations.py +54 -0
  23. OpenApiLibCore/validation.py +497 -0
  24. OpenApiLibCore/value_utils.py +528 -481
  25. openapi_libgen/__init__.py +46 -0
  26. openapi_libgen/command_line.py +87 -0
  27. openapi_libgen/parsing_utils.py +26 -0
  28. openapi_libgen/spec_parser.py +212 -0
  29. openapi_libgen/templates/__init__.jinja +3 -0
  30. openapi_libgen/templates/library.jinja +30 -0
  31. robotframework_openapitools-1.0.0b1.dist-info/METADATA +237 -0
  32. robotframework_openapitools-1.0.0b1.dist-info/RECORD +37 -0
  33. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/WHEEL +1 -1
  34. robotframework_openapitools-1.0.0b1.dist-info/entry_points.txt +3 -0
  35. roboswag/__init__.py +0 -9
  36. roboswag/__main__.py +0 -3
  37. roboswag/auth.py +0 -44
  38. roboswag/cli.py +0 -80
  39. roboswag/core.py +0 -85
  40. roboswag/generate/__init__.py +0 -1
  41. roboswag/generate/generate.py +0 -121
  42. roboswag/generate/models/__init__.py +0 -0
  43. roboswag/generate/models/api.py +0 -219
  44. roboswag/generate/models/definition.py +0 -28
  45. roboswag/generate/models/endpoint.py +0 -68
  46. roboswag/generate/models/parameter.py +0 -25
  47. roboswag/generate/models/response.py +0 -8
  48. roboswag/generate/models/tag.py +0 -16
  49. roboswag/generate/models/utils.py +0 -60
  50. roboswag/generate/templates/api_init.jinja +0 -15
  51. roboswag/generate/templates/models.jinja +0 -7
  52. roboswag/generate/templates/paths.jinja +0 -68
  53. roboswag/logger.py +0 -33
  54. roboswag/validate/__init__.py +0 -6
  55. roboswag/validate/core.py +0 -3
  56. roboswag/validate/schema.py +0 -21
  57. roboswag/validate/text_response.py +0 -14
  58. robotframework_openapitools-0.4.0.dist-info/METADATA +0 -42
  59. robotframework_openapitools-0.4.0.dist-info/RECORD +0 -41
  60. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0b1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,269 @@
1
+ """
2
+ Module holding the functions related to (json) data generation
3
+ for the body of requests made as part of keyword exection.
4
+ """
5
+
6
+ from random import choice, randint, sample
7
+ from typing import Any
8
+
9
+ from robot.api import logger
10
+
11
+ import OpenApiLibCore.path_functions as pf
12
+ from OpenApiLibCore.annotations import JSON
13
+ from OpenApiLibCore.dto_base import (
14
+ Dto,
15
+ IdDependency,
16
+ PropertyValueConstraint,
17
+ )
18
+ from OpenApiLibCore.dto_utils import DefaultDto
19
+ from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
20
+ from OpenApiLibCore.protocols import GetIdPropertyNameType
21
+ from OpenApiLibCore.value_utils import IGNORE, get_valid_value
22
+
23
+
24
+ def get_json_data_for_dto_class(
25
+ schema: dict[str, Any],
26
+ dto_class: type[Dto],
27
+ get_id_property_name: GetIdPropertyNameType,
28
+ operation_id: str = "",
29
+ ) -> JSON:
30
+ match schema.get("type"):
31
+ case "object":
32
+ return get_dict_data_for_dto_class(
33
+ schema=schema,
34
+ dto_class=dto_class,
35
+ get_id_property_name=get_id_property_name,
36
+ operation_id=operation_id,
37
+ )
38
+ case "array":
39
+ return get_list_data_for_dto_class(
40
+ schema=schema,
41
+ dto_class=dto_class,
42
+ get_id_property_name=get_id_property_name,
43
+ operation_id=operation_id,
44
+ )
45
+ case _:
46
+ return get_valid_value(value_schema=schema)
47
+
48
+
49
+ def get_dict_data_for_dto_class(
50
+ schema: dict[str, Any],
51
+ dto_class: type[Dto],
52
+ get_id_property_name: GetIdPropertyNameType,
53
+ operation_id: str = "",
54
+ ) -> dict[str, Any]:
55
+ json_data: dict[str, Any] = {}
56
+
57
+ property_names = get_property_names_to_process(schema=schema, dto_class=dto_class)
58
+
59
+ for property_name in property_names:
60
+ property_schema = schema["properties"][property_name]
61
+ if property_schema.get("readOnly", False):
62
+ continue
63
+
64
+ json_data[property_name] = get_data_for_property(
65
+ property_name=property_name,
66
+ property_schema=property_schema,
67
+ get_id_property_name=get_id_property_name,
68
+ dto_class=dto_class,
69
+ operation_id=operation_id,
70
+ )
71
+
72
+ return json_data
73
+
74
+
75
+ def get_list_data_for_dto_class(
76
+ schema: dict[str, Any],
77
+ dto_class: type[Dto],
78
+ get_id_property_name: GetIdPropertyNameType,
79
+ operation_id: str = "",
80
+ ) -> list[Any]:
81
+ json_data: list[Any] = []
82
+ list_item_schema = schema.get("items", {})
83
+ min_items = schema.get("minItems", 0)
84
+ max_items = schema.get("maxItems", 1)
85
+ number_of_items_to_generate = randint(min_items, max_items)
86
+ for _ in range(number_of_items_to_generate):
87
+ list_item_data = get_json_data_for_dto_class(
88
+ schema=list_item_schema,
89
+ dto_class=dto_class,
90
+ get_id_property_name=get_id_property_name,
91
+ operation_id=operation_id,
92
+ )
93
+ json_data.append(list_item_data)
94
+ return json_data
95
+
96
+
97
+ def get_data_for_property(
98
+ property_name: str,
99
+ property_schema: dict[str, Any],
100
+ get_id_property_name: GetIdPropertyNameType,
101
+ dto_class: type[Dto],
102
+ operation_id: str,
103
+ ) -> JSON:
104
+ property_type = property_schema.get("type")
105
+ if property_type is None:
106
+ property_types = property_schema.get("types")
107
+ if property_types is None:
108
+ if property_schema.get("properties") is None:
109
+ raise NotImplementedError
110
+
111
+ nested_data = get_json_data_for_dto_class(
112
+ schema=property_schema,
113
+ dto_class=DefaultDto,
114
+ get_id_property_name=get_id_property_name,
115
+ )
116
+ return nested_data
117
+
118
+ selected_type_schema = choice(property_types)
119
+ property_type = selected_type_schema["type"]
120
+ property_schema = selected_type_schema
121
+
122
+ if constrained_values := get_constrained_values(
123
+ dto_class=dto_class, property_name=property_name
124
+ ):
125
+ constrained_value = choice(constrained_values)
126
+ # Check if the chosen value is a nested Dto; since a Dto is never
127
+ # instantiated, we can use isinstance(..., type) for this.
128
+ if isinstance(constrained_value, type):
129
+ return get_value_constrained_by_nested_dto(
130
+ property_schema=property_schema,
131
+ nested_dto_class=constrained_value,
132
+ get_id_property_name=get_id_property_name,
133
+ operation_id=operation_id,
134
+ )
135
+ return constrained_value
136
+
137
+ if (
138
+ dependent_id := get_dependent_id(
139
+ dto_class=dto_class,
140
+ property_name=property_name,
141
+ operation_id=operation_id,
142
+ get_id_property_name=get_id_property_name,
143
+ )
144
+ ) is not None:
145
+ return dependent_id
146
+
147
+ if property_type == "object":
148
+ object_data = get_json_data_for_dto_class(
149
+ schema=property_schema,
150
+ dto_class=DefaultDto,
151
+ get_id_property_name=get_id_property_name,
152
+ operation_id="",
153
+ )
154
+ return object_data
155
+
156
+ if property_type == "array":
157
+ array_data = get_json_data_for_dto_class(
158
+ schema=property_schema["items"],
159
+ dto_class=DefaultDto,
160
+ get_id_property_name=get_id_property_name,
161
+ operation_id=operation_id,
162
+ )
163
+ return [array_data]
164
+
165
+ return get_valid_value(property_schema)
166
+
167
+
168
+ def get_value_constrained_by_nested_dto(
169
+ property_schema: dict[str, Any],
170
+ nested_dto_class: type[Dto],
171
+ get_id_property_name: GetIdPropertyNameType,
172
+ operation_id: str,
173
+ ) -> JSON:
174
+ nested_schema = get_schema_for_nested_dto(property_schema=property_schema)
175
+ nested_value = get_json_data_for_dto_class(
176
+ schema=nested_schema,
177
+ dto_class=nested_dto_class,
178
+ get_id_property_name=get_id_property_name,
179
+ operation_id=operation_id,
180
+ )
181
+ return nested_value
182
+
183
+
184
+ def get_schema_for_nested_dto(property_schema: dict[str, Any]) -> dict[str, Any]:
185
+ if property_schema.get("type"):
186
+ return property_schema
187
+
188
+ if possible_types := property_schema.get("types"):
189
+ return choice(possible_types)
190
+
191
+ raise NotImplementedError
192
+
193
+
194
+ def get_property_names_to_process(
195
+ schema: dict[str, Any],
196
+ dto_class: type[Dto],
197
+ ) -> list[str]:
198
+ property_names = []
199
+
200
+ for property_name in schema.get("properties", []):
201
+ # register the oas_name
202
+ _ = get_safe_name_for_oas_name(property_name)
203
+ if constrained_values := get_constrained_values(
204
+ dto_class=dto_class, property_name=property_name
205
+ ):
206
+ # do not add properties that are configured to be ignored
207
+ if IGNORE in constrained_values: # type: ignore[comparison-overlap]
208
+ continue
209
+ property_names.append(property_name)
210
+
211
+ max_properties = schema.get("maxProperties")
212
+ if max_properties and len(property_names) > max_properties:
213
+ required_properties = schema.get("required", [])
214
+ number_of_optional_properties = max_properties - len(required_properties)
215
+ optional_properties = [
216
+ name for name in property_names if name not in required_properties
217
+ ]
218
+ selected_optional_properties = sample(
219
+ optional_properties, number_of_optional_properties
220
+ )
221
+ property_names = required_properties + selected_optional_properties
222
+
223
+ return property_names
224
+
225
+
226
+ def get_constrained_values(
227
+ dto_class: type[Dto], property_name: str
228
+ ) -> list[JSON | type[Dto]]:
229
+ relations = dto_class.get_relations()
230
+ values_list = [
231
+ c.values
232
+ for c in relations
233
+ if (isinstance(c, PropertyValueConstraint) and c.property_name == property_name)
234
+ ]
235
+ # values should be empty or contain 1 list of allowed values
236
+ return values_list.pop() if values_list else []
237
+
238
+
239
+ def get_dependent_id(
240
+ dto_class: type[Dto],
241
+ property_name: str,
242
+ operation_id: str,
243
+ get_id_property_name: GetIdPropertyNameType,
244
+ ) -> str | int | float | None:
245
+ relations = dto_class.get_relations()
246
+ # multiple get paths are possible based on the operation being performed
247
+ id_get_paths = [
248
+ (d.get_path, d.operation_id)
249
+ for d in relations
250
+ if (isinstance(d, IdDependency) and d.property_name == property_name)
251
+ ]
252
+ if not id_get_paths:
253
+ return None
254
+ if len(id_get_paths) == 1:
255
+ id_get_path, _ = id_get_paths.pop()
256
+ else:
257
+ try:
258
+ [id_get_path] = [
259
+ path for path, operation in id_get_paths if operation == operation_id
260
+ ]
261
+ # There could be multiple get_paths, but not one for the current operation
262
+ except ValueError:
263
+ return None
264
+
265
+ valid_id = pf.get_valid_id_for_path(
266
+ path=id_get_path, get_id_property_name=get_id_property_name
267
+ )
268
+ logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}")
269
+ return valid_id
@@ -0,0 +1,240 @@
1
+ """
2
+ Module holding the main functions related to data generation
3
+ for the requests made as part of keyword exection.
4
+ """
5
+
6
+ import re
7
+ from dataclasses import Field, field, make_dataclass
8
+ from random import choice
9
+ from typing import Any, cast
10
+
11
+ from robot.api import logger
12
+
13
+ import OpenApiLibCore.path_functions as pf
14
+ from OpenApiLibCore.annotations import JSON
15
+ from OpenApiLibCore.dto_base import (
16
+ Dto,
17
+ PropertyValueConstraint,
18
+ ResourceRelation,
19
+ resolve_schema,
20
+ )
21
+ from OpenApiLibCore.dto_utils import DefaultDto
22
+ from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
23
+ from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
24
+ from OpenApiLibCore.request_data import RequestData
25
+ from OpenApiLibCore.value_utils import IGNORE, get_valid_value
26
+
27
+ from .body_data_generation import (
28
+ get_json_data_for_dto_class as _get_json_data_for_dto_class,
29
+ )
30
+
31
+
32
+ def get_request_data(
33
+ path: str,
34
+ method: str,
35
+ get_dto_class: GetDtoClassType,
36
+ get_id_property_name: GetIdPropertyNameType,
37
+ openapi_spec: dict[str, Any],
38
+ ) -> RequestData:
39
+ method = method.lower()
40
+ dto_cls_name = get_dto_cls_name(path=path, method=method)
41
+ # The path can contain already resolved Ids that have to be matched
42
+ # against the parametrized paths in the paths section.
43
+ spec_path = pf.get_parametrized_path(path=path, openapi_spec=openapi_spec)
44
+ dto_class = get_dto_class(path=spec_path, method=method)
45
+ try:
46
+ method_spec = openapi_spec["paths"][spec_path][method]
47
+ except KeyError:
48
+ logger.info(
49
+ f"method '{method}' not supported on '{spec_path}, using empty spec."
50
+ )
51
+ method_spec = {}
52
+
53
+ parameters, params, headers = get_request_parameters(
54
+ dto_class=dto_class, method_spec=method_spec
55
+ )
56
+ if (body_spec := method_spec.get("requestBody", None)) is None:
57
+ dto_instance = _get_dto_instance_for_empty_body(
58
+ dto_class=dto_class,
59
+ dto_cls_name=dto_cls_name,
60
+ method_spec=method_spec,
61
+ )
62
+ return RequestData(
63
+ dto=dto_instance,
64
+ parameters=parameters,
65
+ params=params,
66
+ headers=headers,
67
+ has_body=False,
68
+ )
69
+
70
+ headers.update({"content-type": get_content_type(body_spec)})
71
+
72
+ content_schema = resolve_schema(get_content_schema(body_spec))
73
+ dto_data = _get_json_data_for_dto_class(
74
+ schema=content_schema,
75
+ dto_class=dto_class,
76
+ get_id_property_name=get_id_property_name,
77
+ operation_id=method_spec.get("operationId", ""),
78
+ )
79
+ dto_instance = _get_dto_instance_from_dto_data(
80
+ content_schema=content_schema,
81
+ dto_class=dto_class,
82
+ dto_data=dto_data,
83
+ method_spec=method_spec,
84
+ dto_cls_name=dto_cls_name,
85
+ )
86
+ return RequestData(
87
+ dto=dto_instance,
88
+ dto_schema=content_schema,
89
+ parameters=parameters,
90
+ params=params,
91
+ headers=headers,
92
+ )
93
+
94
+
95
+ def _get_dto_instance_for_empty_body(
96
+ dto_class: type[Dto],
97
+ dto_cls_name: str,
98
+ method_spec: dict[str, Any],
99
+ ) -> Dto:
100
+ if dto_class == DefaultDto:
101
+ dto_instance: Dto = DefaultDto()
102
+ else:
103
+ dto_class = make_dataclass(
104
+ cls_name=method_spec.get("operationId", dto_cls_name),
105
+ fields=[],
106
+ bases=(dto_class,),
107
+ )
108
+ dto_instance = dto_class()
109
+ return dto_instance
110
+
111
+
112
+ def _get_dto_instance_from_dto_data(
113
+ content_schema: dict[str, Any],
114
+ dto_class: type[Dto],
115
+ dto_data: JSON,
116
+ method_spec: dict[str, Any],
117
+ dto_cls_name: str,
118
+ ) -> Dto:
119
+ if not isinstance(dto_data, (dict, list)):
120
+ return DefaultDto()
121
+
122
+ if isinstance(dto_data, list):
123
+ raise NotImplementedError
124
+
125
+ fields = get_fields_from_dto_data(content_schema, dto_data)
126
+ dto_class_ = make_dataclass(
127
+ cls_name=method_spec.get("operationId", dto_cls_name),
128
+ fields=fields,
129
+ bases=(dto_class,),
130
+ )
131
+ dto_data = {get_safe_key(key): value for key, value in dto_data.items()}
132
+ return cast(Dto, dto_class_(**dto_data))
133
+
134
+
135
+ def get_fields_from_dto_data(
136
+ content_schema: dict[str, Any], dto_data: dict[str, JSON]
137
+ ) -> list[tuple[str, type[Any], Field[Any]]]:
138
+ """Get a dataclasses fields list based on the content_schema and dto_data."""
139
+ fields: list[tuple[str, type[Any], Field[Any]]] = []
140
+ for key, value in dto_data.items():
141
+ required_properties = content_schema.get("required", [])
142
+ safe_key = get_safe_key(key)
143
+ metadata = {"original_property_name": key}
144
+ if key in required_properties:
145
+ # The fields list is used to create a dataclass, so non-default fields
146
+ # must go before fields with a default
147
+ field_ = cast(Field[Any], field(metadata=metadata)) # pylint: disable=invalid-field-call
148
+ fields.insert(0, (safe_key, type(value), field_))
149
+ else:
150
+ field_ = cast(Field[Any], field(default=None, metadata=metadata)) # pylint: disable=invalid-field-call
151
+ fields.append((safe_key, type(value), field_))
152
+ return fields
153
+
154
+
155
+ def get_safe_key(key: str) -> str:
156
+ """
157
+ Helper function to convert a valid JSON property name to a string that can be used
158
+ as a Python variable or function / method name.
159
+ """
160
+ key = key.replace("-", "_")
161
+ key = key.replace("@", "_")
162
+ if key[0].isdigit():
163
+ key = f"_{key}"
164
+ return key
165
+
166
+
167
+ def get_dto_cls_name(path: str, method: str) -> str:
168
+ method = method.capitalize()
169
+ path = path.translate({ord(i): None for i in "{}"})
170
+ path_parts = path.split("/")
171
+ path_parts = [p.capitalize() for p in path_parts]
172
+ result = "".join([method, *path_parts])
173
+ return result
174
+
175
+
176
+ def get_content_schema(body_spec: dict[str, Any]) -> dict[str, Any]:
177
+ """Get the content schema from the requestBody spec."""
178
+ content_type = get_content_type(body_spec)
179
+ content_schema = body_spec["content"][content_type]["schema"]
180
+ return resolve_schema(content_schema)
181
+
182
+
183
+ def get_content_type(body_spec: dict[str, Any]) -> str:
184
+ """Get and validate the first supported content type from the requested body spec
185
+
186
+ Should be application/json like content type,
187
+ e.g "application/json;charset=utf-8" or "application/merge-patch+json"
188
+ """
189
+ content_types: list[str] = body_spec["content"].keys()
190
+ json_regex = r"application/([a-z\-]+\+)?json(;\s?charset=(.+))?"
191
+ for content_type in content_types:
192
+ if re.search(json_regex, content_type):
193
+ return content_type
194
+
195
+ # At present no supported for other types.
196
+ raise NotImplementedError(
197
+ f"Only content types like 'application/json' are supported. "
198
+ f"Content types definded in the spec are '{content_types}'."
199
+ )
200
+
201
+
202
+ def get_request_parameters(
203
+ dto_class: Dto | type[Dto], method_spec: dict[str, Any]
204
+ ) -> tuple[list[dict[str, Any]], dict[str, Any], dict[str, str]]:
205
+ """Get the methods parameter spec and params and headers with valid data."""
206
+ parameters = method_spec.get("parameters", [])
207
+ parameter_relations = dto_class.get_parameter_relations()
208
+ query_params = [p for p in parameters if p.get("in") == "query"]
209
+ header_params = [p for p in parameters if p.get("in") == "header"]
210
+ params = get_parameter_data(query_params, parameter_relations)
211
+ headers = get_parameter_data(header_params, parameter_relations)
212
+ return parameters, params, headers
213
+
214
+
215
+ def get_parameter_data(
216
+ parameters: list[dict[str, Any]],
217
+ parameter_relations: list[ResourceRelation],
218
+ ) -> dict[str, str]:
219
+ """Generate a valid list of key-value pairs for all parameters."""
220
+ result: dict[str, str] = {}
221
+ value: Any = None
222
+ for parameter in parameters:
223
+ parameter_name = parameter["name"]
224
+ # register the oas_name
225
+ _ = get_safe_name_for_oas_name(parameter_name)
226
+ parameter_schema = resolve_schema(parameter["schema"])
227
+ relations = [
228
+ r for r in parameter_relations if r.property_name == parameter_name
229
+ ]
230
+ if constrained_values := [
231
+ r.values for r in relations if isinstance(r, PropertyValueConstraint)
232
+ ]:
233
+ value = choice(*constrained_values)
234
+ if value is IGNORE:
235
+ continue
236
+ result[parameter_name] = value
237
+ continue
238
+ value = get_valid_value(parameter_schema)
239
+ result[parameter_name] = value
240
+ return result