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
@@ -0,0 +1,250 @@
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 _path_functions
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.models import (
20
+ ArraySchema,
21
+ ObjectSchema,
22
+ SchemaObjectTypes,
23
+ UnionTypeSchema,
24
+ )
25
+ from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
26
+ from OpenApiLibCore.protocols import GetIdPropertyNameType
27
+ from OpenApiLibCore.value_utils import IGNORE
28
+
29
+
30
+ def get_json_data_for_dto_class(
31
+ schema: SchemaObjectTypes,
32
+ dto_class: type[Dto],
33
+ get_id_property_name: GetIdPropertyNameType,
34
+ operation_id: str | None = None,
35
+ ) -> JSON:
36
+ if isinstance(schema, UnionTypeSchema):
37
+ chosen_schema = choice(schema.resolved_schemas)
38
+ return get_json_data_for_dto_class(
39
+ schema=chosen_schema,
40
+ dto_class=dto_class,
41
+ get_id_property_name=get_id_property_name,
42
+ operation_id=operation_id,
43
+ )
44
+
45
+ match schema:
46
+ case ObjectSchema():
47
+ return get_dict_data_for_dto_class(
48
+ schema=schema,
49
+ dto_class=dto_class,
50
+ get_id_property_name=get_id_property_name,
51
+ operation_id=operation_id,
52
+ )
53
+ case ArraySchema():
54
+ return get_list_data_for_dto_class(
55
+ schema=schema,
56
+ dto_class=dto_class,
57
+ get_id_property_name=get_id_property_name,
58
+ operation_id=operation_id,
59
+ )
60
+ case _:
61
+ return schema.get_valid_value()
62
+
63
+
64
+ def get_dict_data_for_dto_class(
65
+ schema: ObjectSchema,
66
+ dto_class: type[Dto],
67
+ get_id_property_name: GetIdPropertyNameType,
68
+ operation_id: str | None = None,
69
+ ) -> dict[str, Any]:
70
+ json_data: dict[str, Any] = {}
71
+
72
+ property_names = get_property_names_to_process(schema=schema, dto_class=dto_class)
73
+
74
+ for property_name in property_names:
75
+ property_schema = schema.properties.root[property_name] # type: ignore[union-attr]
76
+ if property_schema.readOnly:
77
+ continue
78
+
79
+ json_data[property_name] = get_data_for_property(
80
+ property_name=property_name,
81
+ property_schema=property_schema,
82
+ get_id_property_name=get_id_property_name,
83
+ dto_class=dto_class,
84
+ operation_id=operation_id,
85
+ )
86
+
87
+ return json_data
88
+
89
+
90
+ def get_list_data_for_dto_class(
91
+ schema: ArraySchema,
92
+ dto_class: type[Dto],
93
+ get_id_property_name: GetIdPropertyNameType,
94
+ operation_id: str | None = None,
95
+ ) -> list[JSON]:
96
+ json_data: list[JSON] = []
97
+ list_item_schema = schema.items
98
+ min_items = schema.minItems if schema.minItems is not None else 0
99
+ max_items = schema.maxItems if schema.maxItems is not None else 1
100
+ number_of_items_to_generate = randint(min_items, max_items)
101
+ for _ in range(number_of_items_to_generate):
102
+ list_item_data = get_json_data_for_dto_class(
103
+ schema=list_item_schema,
104
+ dto_class=dto_class,
105
+ get_id_property_name=get_id_property_name,
106
+ operation_id=operation_id,
107
+ )
108
+ json_data.append(list_item_data)
109
+ return json_data
110
+
111
+
112
+ def get_data_for_property(
113
+ property_name: str,
114
+ property_schema: SchemaObjectTypes,
115
+ get_id_property_name: GetIdPropertyNameType,
116
+ dto_class: type[Dto],
117
+ operation_id: str | None,
118
+ ) -> JSON:
119
+ if constrained_values := get_constrained_values(
120
+ dto_class=dto_class, property_name=property_name
121
+ ):
122
+ constrained_value = choice(constrained_values)
123
+ # Check if the chosen value is a nested Dto; since a Dto is never
124
+ # instantiated, we can use isinstance(..., type) for this.
125
+ if isinstance(constrained_value, type):
126
+ return get_value_constrained_by_nested_dto(
127
+ property_schema=property_schema,
128
+ nested_dto_class=constrained_value,
129
+ get_id_property_name=get_id_property_name,
130
+ operation_id=operation_id,
131
+ )
132
+ return constrained_value
133
+
134
+ if (
135
+ dependent_id := get_dependent_id(
136
+ dto_class=dto_class,
137
+ property_name=property_name,
138
+ operation_id=operation_id,
139
+ get_id_property_name=get_id_property_name,
140
+ )
141
+ ) is not None:
142
+ return dependent_id
143
+
144
+ return get_json_data_for_dto_class(
145
+ schema=property_schema,
146
+ dto_class=DefaultDto,
147
+ get_id_property_name=get_id_property_name,
148
+ )
149
+
150
+
151
+ def get_value_constrained_by_nested_dto(
152
+ property_schema: SchemaObjectTypes,
153
+ nested_dto_class: type[Dto],
154
+ get_id_property_name: GetIdPropertyNameType,
155
+ operation_id: str | None,
156
+ ) -> JSON:
157
+ nested_schema = get_schema_for_nested_dto(property_schema=property_schema)
158
+ nested_value = get_json_data_for_dto_class(
159
+ schema=nested_schema,
160
+ dto_class=nested_dto_class,
161
+ get_id_property_name=get_id_property_name,
162
+ operation_id=operation_id,
163
+ )
164
+ return nested_value
165
+
166
+
167
+ def get_schema_for_nested_dto(property_schema: SchemaObjectTypes) -> SchemaObjectTypes:
168
+ if isinstance(property_schema, UnionTypeSchema):
169
+ chosen_schema = choice(property_schema.resolved_schemas)
170
+ return get_schema_for_nested_dto(chosen_schema)
171
+
172
+ return property_schema
173
+
174
+
175
+ def get_property_names_to_process(
176
+ schema: ObjectSchema,
177
+ dto_class: type[Dto],
178
+ ) -> list[str]:
179
+ property_names = []
180
+
181
+ for property_name in schema.properties.root: # type: ignore[union-attr]
182
+ # register the oas_name
183
+ _ = get_safe_name_for_oas_name(property_name)
184
+ if constrained_values := get_constrained_values(
185
+ dto_class=dto_class, property_name=property_name
186
+ ):
187
+ # do not add properties that are configured to be ignored
188
+ if IGNORE in constrained_values: # type: ignore[comparison-overlap]
189
+ continue
190
+ property_names.append(property_name)
191
+
192
+ max_properties = schema.maxProperties
193
+ if max_properties and len(property_names) > max_properties:
194
+ required_properties = schema.required
195
+ number_of_optional_properties = max_properties - len(required_properties)
196
+ optional_properties = [
197
+ name for name in property_names if name not in required_properties
198
+ ]
199
+ selected_optional_properties = sample(
200
+ optional_properties, number_of_optional_properties
201
+ )
202
+ property_names = required_properties + selected_optional_properties
203
+
204
+ return property_names
205
+
206
+
207
+ def get_constrained_values(
208
+ dto_class: type[Dto], property_name: str
209
+ ) -> list[JSON | type[Dto]]:
210
+ relations = dto_class.get_relations()
211
+ values_list = [
212
+ c.values
213
+ for c in relations
214
+ if (isinstance(c, PropertyValueConstraint) and c.property_name == property_name)
215
+ ]
216
+ # values should be empty or contain 1 list of allowed values
217
+ return values_list.pop() if values_list else []
218
+
219
+
220
+ def get_dependent_id(
221
+ dto_class: type[Dto],
222
+ property_name: str,
223
+ operation_id: str | None,
224
+ get_id_property_name: GetIdPropertyNameType,
225
+ ) -> str | int | float | None:
226
+ relations = dto_class.get_relations()
227
+ # multiple get paths are possible based on the operation being performed
228
+ id_get_paths = [
229
+ (d.get_path, d.operation_id)
230
+ for d in relations
231
+ if (isinstance(d, IdDependency) and d.property_name == property_name)
232
+ ]
233
+ if not id_get_paths:
234
+ return None
235
+ if len(id_get_paths) == 1:
236
+ id_get_path, _ = id_get_paths.pop()
237
+ else:
238
+ try:
239
+ [id_get_path] = [
240
+ path for path, operation in id_get_paths if operation == operation_id
241
+ ]
242
+ # There could be multiple get_paths, but not one for the current operation
243
+ except ValueError:
244
+ return None
245
+
246
+ valid_id = _path_functions.get_valid_id_for_path(
247
+ path=id_get_path, get_id_property_name=get_id_property_name
248
+ )
249
+ logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}")
250
+ return valid_id
@@ -0,0 +1,233 @@
1
+ """
2
+ Module holding the main functions related to data generation
3
+ for the requests made as part of keyword exection.
4
+ """
5
+
6
+ from dataclasses import Field, field, make_dataclass
7
+ from random import choice
8
+ from typing import Any, cast
9
+
10
+ from robot.api import logger
11
+
12
+ import OpenApiLibCore.path_functions as _path_functions
13
+ from OpenApiLibCore.annotations import JSON
14
+ from OpenApiLibCore.dto_base import (
15
+ Dto,
16
+ PropertyValueConstraint,
17
+ ResourceRelation,
18
+ )
19
+ from OpenApiLibCore.dto_utils import DefaultDto
20
+ from OpenApiLibCore.models import (
21
+ ObjectSchema,
22
+ OpenApiObject,
23
+ OperationObject,
24
+ ParameterObject,
25
+ UnionTypeSchema,
26
+ )
27
+ from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
28
+ from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
29
+ from OpenApiLibCore.request_data import RequestData
30
+ from OpenApiLibCore.value_utils import IGNORE
31
+
32
+ from .body_data_generation import (
33
+ get_json_data_for_dto_class as _get_json_data_for_dto_class,
34
+ )
35
+
36
+
37
+ def get_request_data(
38
+ path: str,
39
+ method: str,
40
+ get_dto_class: GetDtoClassType,
41
+ get_id_property_name: GetIdPropertyNameType,
42
+ openapi_spec: OpenApiObject,
43
+ ) -> RequestData:
44
+ method = method.lower()
45
+ dto_cls_name = get_dto_cls_name(path=path, method=method)
46
+ # The path can contain already resolved Ids that have to be matched
47
+ # against the parametrized paths in the paths section.
48
+ spec_path = _path_functions.get_parametrized_path(
49
+ path=path, openapi_spec=openapi_spec
50
+ )
51
+ dto_class = get_dto_class(path=spec_path, method=method)
52
+ try:
53
+ path_item = openapi_spec.paths[spec_path]
54
+ operation_spec: OperationObject | None = getattr(path_item, method)
55
+ if operation_spec is None:
56
+ raise AttributeError
57
+ except AttributeError:
58
+ logger.info(
59
+ f"method '{method}' not supported on '{spec_path}, using empty spec."
60
+ )
61
+ operation_spec = OperationObject(operationId="")
62
+
63
+ parameters, params, headers = get_request_parameters(
64
+ dto_class=dto_class, method_spec=operation_spec
65
+ )
66
+ if operation_spec.requestBody is None:
67
+ dto_instance = _get_dto_instance_for_empty_body(
68
+ dto_class=dto_class,
69
+ dto_cls_name=dto_cls_name,
70
+ method_spec=operation_spec,
71
+ )
72
+ return RequestData(
73
+ dto=dto_instance,
74
+ parameters=parameters,
75
+ params=params,
76
+ headers=headers,
77
+ has_body=False,
78
+ )
79
+
80
+ body_schema = operation_spec.requestBody.schema_
81
+
82
+ if not body_schema:
83
+ raise ValueError(
84
+ f"No supported content schema found: {operation_spec.requestBody.content}"
85
+ )
86
+
87
+ headers.update({"content-type": operation_spec.requestBody.mime_type})
88
+
89
+ if isinstance(body_schema, UnionTypeSchema):
90
+ resolved_schemas = body_schema.resolved_schemas
91
+ body_schema = choice(resolved_schemas)
92
+
93
+ if not isinstance(body_schema, ObjectSchema):
94
+ raise ValueError(f"Selected schema is not an object schema: {body_schema}")
95
+
96
+ dto_data = _get_json_data_for_dto_class(
97
+ schema=body_schema,
98
+ dto_class=dto_class,
99
+ get_id_property_name=get_id_property_name,
100
+ operation_id=operation_spec.operationId,
101
+ )
102
+ dto_instance = _get_dto_instance_from_dto_data(
103
+ object_schema=body_schema,
104
+ dto_class=dto_class,
105
+ dto_data=dto_data,
106
+ method_spec=operation_spec,
107
+ dto_cls_name=dto_cls_name,
108
+ )
109
+ return RequestData(
110
+ dto=dto_instance,
111
+ body_schema=body_schema,
112
+ parameters=parameters,
113
+ params=params,
114
+ headers=headers,
115
+ )
116
+
117
+
118
+ def _get_dto_instance_for_empty_body(
119
+ dto_class: type[Dto],
120
+ dto_cls_name: str,
121
+ method_spec: OperationObject,
122
+ ) -> Dto:
123
+ if dto_class == DefaultDto:
124
+ dto_instance: Dto = DefaultDto()
125
+ else:
126
+ cls_name = method_spec.operationId if method_spec.operationId else dto_cls_name
127
+ dto_class = make_dataclass(
128
+ cls_name=cls_name,
129
+ fields=[],
130
+ bases=(dto_class,),
131
+ )
132
+ dto_instance = dto_class()
133
+ return dto_instance
134
+
135
+
136
+ def _get_dto_instance_from_dto_data(
137
+ object_schema: ObjectSchema,
138
+ dto_class: type[Dto],
139
+ dto_data: JSON,
140
+ method_spec: OperationObject,
141
+ dto_cls_name: str,
142
+ ) -> Dto:
143
+ if not isinstance(dto_data, (dict, list)):
144
+ return DefaultDto()
145
+
146
+ if isinstance(dto_data, list):
147
+ raise NotImplementedError
148
+
149
+ fields = get_fields_from_dto_data(object_schema, dto_data)
150
+ cls_name = method_spec.operationId if method_spec.operationId else dto_cls_name
151
+ dto_class_ = make_dataclass(
152
+ cls_name=cls_name,
153
+ fields=fields,
154
+ bases=(dto_class,),
155
+ )
156
+ # dto_data = {get_safe_key(key): value for key, value in dto_data.items()}
157
+ dto_data = {
158
+ get_safe_name_for_oas_name(key): value for key, value in dto_data.items()
159
+ }
160
+ return cast(Dto, dto_class_(**dto_data))
161
+
162
+
163
+ def get_fields_from_dto_data(
164
+ object_schema: ObjectSchema, dto_data: dict[str, JSON]
165
+ ) -> list[tuple[str, type[object], Field[object]]]:
166
+ """Get a dataclasses fields list based on the content_schema and dto_data."""
167
+ fields: list[tuple[str, type[object], Field[object]]] = []
168
+
169
+ for key, value in dto_data.items():
170
+ # safe_key = get_safe_key(key)
171
+ safe_key = get_safe_name_for_oas_name(key)
172
+ # metadata = {"original_property_name": key}
173
+ if key in object_schema.required:
174
+ # The fields list is used to create a dataclass, so non-default fields
175
+ # must go before fields with a default
176
+ field_ = cast(Field[Any], field()) # pylint: disable=invalid-field-call
177
+ fields.insert(0, (safe_key, type(value), field_))
178
+ else:
179
+ field_ = cast(Field[Any], field(default=None)) # pylint: disable=invalid-field-call
180
+ fields.append((safe_key, type(value), field_))
181
+ return fields
182
+
183
+
184
+ def get_dto_cls_name(path: str, method: str) -> str:
185
+ method = method.capitalize()
186
+ path = path.translate({ord(i): None for i in "{}"})
187
+ path_parts = path.split("/")
188
+ path_parts = [p.capitalize() for p in path_parts]
189
+ result = "".join([method, *path_parts])
190
+ return result
191
+
192
+
193
+ def get_request_parameters(
194
+ dto_class: Dto | type[Dto], method_spec: OperationObject
195
+ ) -> tuple[list[ParameterObject], dict[str, Any], dict[str, str]]:
196
+ """Get the methods parameter spec and params and headers with valid data."""
197
+ parameters = method_spec.parameters if method_spec.parameters else []
198
+ parameter_relations = dto_class.get_parameter_relations()
199
+ query_params = [p for p in parameters if p.in_ == "query"]
200
+ header_params = [p for p in parameters if p.in_ == "header"]
201
+ params = get_parameter_data(query_params, parameter_relations)
202
+ headers = get_parameter_data(header_params, parameter_relations)
203
+ return parameters, params, headers
204
+
205
+
206
+ def get_parameter_data(
207
+ parameters: list[ParameterObject],
208
+ parameter_relations: list[ResourceRelation],
209
+ ) -> dict[str, str]:
210
+ """Generate a valid list of key-value pairs for all parameters."""
211
+ result: dict[str, str] = {}
212
+ value: Any = None
213
+ for parameter in parameters:
214
+ parameter_name = parameter.name
215
+ # register the oas_name
216
+ _ = get_safe_name_for_oas_name(parameter_name)
217
+ relations = [
218
+ r for r in parameter_relations if r.property_name == parameter_name
219
+ ]
220
+ if constrained_values := [
221
+ r.values for r in relations if isinstance(r, PropertyValueConstraint)
222
+ ]:
223
+ value = choice(*constrained_values)
224
+ if value is IGNORE:
225
+ continue
226
+ result[parameter_name] = value
227
+ continue
228
+
229
+ if parameter.schema_ is None:
230
+ continue
231
+ value = parameter.schema_.get_valid_value()
232
+ result[parameter_name] = value
233
+ return result