robotframework-openapitools 0.4.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 +78 -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 +67 -130
  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 +355 -330
  18. OpenApiLibCore/openapi_libcore.py +385 -1953
  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.4.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.4.0.dist-info/METADATA +0 -42
  62. robotframework_openapitools-0.4.0.dist-info/RECORD +0 -41
  63. {robotframework_openapitools-0.4.0.dist-info → robotframework_openapitools-1.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,97 @@
1
+ """
2
+ Module holding the functions to support mapping between Python-safe parameter
3
+ names and the original names in the parsed OpenApi Specification document.
4
+ """
5
+
6
+ from typing import Generator
7
+
8
+ from OpenApiLibCore.models import ParameterObject, PathItemObject
9
+
10
+ PARAMETER_REGISTRY: dict[str, str] = {
11
+ "body": "body",
12
+ }
13
+
14
+
15
+ def get_oas_name_from_safe_name(safe_name: str) -> str:
16
+ oas_name = PARAMETER_REGISTRY.get(safe_name)
17
+ if not oas_name:
18
+ raise ValueError(f"No entry for '{safe_name}' registered {PARAMETER_REGISTRY}.")
19
+ return oas_name
20
+
21
+
22
+ def get_safe_name_for_oas_name(oas_name: str) -> str:
23
+ if _is_python_safe(oas_name):
24
+ PARAMETER_REGISTRY[oas_name] = oas_name
25
+ return oas_name
26
+
27
+ safe_name = convert_string_to_python_identifier(oas_name)
28
+
29
+ if safe_name not in PARAMETER_REGISTRY:
30
+ PARAMETER_REGISTRY[safe_name] = oas_name
31
+ return safe_name
32
+
33
+ registered_oas_name = PARAMETER_REGISTRY[safe_name]
34
+ if registered_oas_name == oas_name:
35
+ return safe_name
36
+ # We're dealing with multiple oas_names that convert to the same safe_name.
37
+ # To resolve this, a more verbose safe_name is generated. This is less user-friendly
38
+ # but necessary to ensure an one-to-one mapping.
39
+ verbose_safe_name = convert_string_to_python_identifier(oas_name, verbose=True)
40
+ if verbose_safe_name not in PARAMETER_REGISTRY:
41
+ PARAMETER_REGISTRY[verbose_safe_name] = oas_name
42
+ return verbose_safe_name
43
+
44
+
45
+ def _is_python_safe(name: str) -> bool:
46
+ return name.isidentifier()
47
+
48
+
49
+ def convert_string_to_python_identifier(string: str, verbose: bool = False) -> str:
50
+ def _convert_string_to_python_identifier() -> Generator[str, None, None]:
51
+ string_iterator = iter(string)
52
+
53
+ # The first character must be A-z or _
54
+ first_character = next(string_iterator, "_")
55
+ if first_character.isalpha() or first_character == "_":
56
+ yield first_character
57
+ elif first_character.isnumeric():
58
+ yield "_" + first_character
59
+ elif not verbose:
60
+ yield "_"
61
+ else:
62
+ ascii_code = ord(first_character)
63
+ yield f"_{ascii_code}_"
64
+ # Further characters must be A-z, 0-9 or _
65
+ for character in string_iterator:
66
+ if character.isalnum() or character == "_":
67
+ yield character
68
+ elif not verbose:
69
+ yield "_"
70
+ else:
71
+ ascii_code = ord(character)
72
+ yield f"_{ascii_code}_"
73
+
74
+ if _is_python_safe(string):
75
+ return string
76
+
77
+ converted_string = "".join(_convert_string_to_python_identifier())
78
+ if not _is_python_safe(converted_string):
79
+ raise ValueError(f"Failed to convert '{string}' to Python identifier.")
80
+ return converted_string
81
+
82
+
83
+ def register_path_parameters(paths_data: dict[str, PathItemObject]) -> None:
84
+ def _register_path_parameter(parameter_object: ParameterObject) -> None:
85
+ if parameter_object.in_ == "path":
86
+ _ = get_safe_name_for_oas_name(parameter_object.name)
87
+
88
+ for path_item in paths_data.values():
89
+ if parameters := path_item.parameters:
90
+ for parameter in path_item.parameters:
91
+ _register_path_parameter(parameter_object=parameter)
92
+
93
+ operations = path_item.get_operations()
94
+ for operation in operations.values():
95
+ if parameters := operation.parameters:
96
+ for parameter in parameters:
97
+ _register_path_parameter(parameter_object=parameter)
@@ -0,0 +1,215 @@
1
+ """Module holding the functions related to paths and urls."""
2
+
3
+ import json as _json
4
+ from itertools import zip_longest
5
+ from random import choice
6
+ from typing import Any
7
+
8
+ from requests import Response
9
+ from robot.libraries.BuiltIn import BuiltIn
10
+
11
+ from OpenApiLibCore.models import OpenApiObject
12
+ from OpenApiLibCore.protocols import GetIdPropertyNameType, GetPathDtoClassType
13
+ from OpenApiLibCore.request_data import RequestData
14
+
15
+ run_keyword = BuiltIn().run_keyword
16
+
17
+
18
+ def match_parts(parts: list[str], spec_parts: list[str]) -> bool:
19
+ for part, spec_part in zip_longest(parts, spec_parts, fillvalue="Filler"):
20
+ if part == "Filler" or spec_part == "Filler":
21
+ return False
22
+ if part != spec_part and not spec_part.startswith("{"):
23
+ return False
24
+ return True
25
+
26
+
27
+ def get_parametrized_path(path: str, openapi_spec: OpenApiObject) -> str:
28
+ path_parts = path.split("/")
29
+ # if the last part is empty, the path has a trailing `/` that
30
+ # should be ignored during matching
31
+ if path_parts[-1] == "":
32
+ _ = path_parts.pop(-1)
33
+
34
+ spec_paths: list[str] = list(openapi_spec.paths.keys())
35
+
36
+ candidates: list[str] = []
37
+
38
+ for spec_path in spec_paths:
39
+ spec_path_parts = spec_path.split("/")
40
+ # ignore trailing `/` the same way as for path_parts
41
+ if spec_path_parts[-1] == "":
42
+ _ = spec_path_parts.pop(-1)
43
+ if match_parts(path_parts, spec_path_parts):
44
+ candidates.append(spec_path)
45
+
46
+ if not candidates:
47
+ raise ValueError(f"{path} not found in paths section of the OpenAPI document.")
48
+
49
+ if len(candidates) == 1:
50
+ return candidates[0]
51
+ # Multiple matches can happen in APIs with overloaded paths, e.g.
52
+ # /users/me
53
+ # /users/${user_id}
54
+ # In this case, find the closest (or exact) match
55
+ exact_match = [c for c in candidates if c == path]
56
+ if exact_match:
57
+ return exact_match[0]
58
+ # TODO: Implement a decision mechanism when real-world examples become available
59
+ # In the face of ambiguity, refuse the temptation to guess.
60
+ raise ValueError(f"{path} matched to multiple paths: {candidates}")
61
+
62
+
63
+ def get_valid_url(
64
+ path: str,
65
+ base_url: str,
66
+ get_path_dto_class: GetPathDtoClassType,
67
+ openapi_spec: OpenApiObject,
68
+ ) -> str:
69
+ try:
70
+ # path can be partially resolved or provided by a PathPropertiesConstraint
71
+ parametrized_path = get_parametrized_path(path=path, openapi_spec=openapi_spec)
72
+ _ = openapi_spec.paths[parametrized_path]
73
+ except KeyError:
74
+ raise ValueError(
75
+ f"{path} not found in paths section of the OpenAPI document."
76
+ ) from None
77
+ dto_class = get_path_dto_class(path=path)
78
+ relations = dto_class.get_path_relations()
79
+ paths = [p.path for p in relations]
80
+ if paths:
81
+ url = f"{base_url}{choice(paths)}"
82
+ return url
83
+ path_parts = list(path.split("/"))
84
+ for index, part in enumerate(path_parts):
85
+ if part.startswith("{") and part.endswith("}"):
86
+ type_path_parts = path_parts[slice(index)]
87
+ type_path = "/".join(type_path_parts)
88
+ existing_id: str | int | float = run_keyword(
89
+ "get_valid_id_for_path", type_path
90
+ )
91
+ path_parts[index] = str(existing_id)
92
+ resolved_path = "/".join(path_parts)
93
+ url = f"{base_url}{resolved_path}"
94
+ return url
95
+
96
+
97
+ def get_valid_id_for_path(
98
+ path: str,
99
+ get_id_property_name: GetIdPropertyNameType,
100
+ ) -> str | int:
101
+ url: str = run_keyword("get_valid_url", path)
102
+ # Try to create a new resource to prevent conflicts caused by
103
+ # operations performed on the same resource by other test cases
104
+ request_data: RequestData = run_keyword("get_request_data", path, "post")
105
+
106
+ response: Response = run_keyword(
107
+ "authorized_request",
108
+ url,
109
+ "post",
110
+ request_data.get_required_params(),
111
+ request_data.get_required_headers(),
112
+ request_data.get_required_properties_dict(),
113
+ )
114
+
115
+ id_property, id_transformer = get_id_property_name(path=path)
116
+
117
+ if not response.ok:
118
+ # If a new resource cannot be created using POST, try to retrieve a
119
+ # valid id using a GET request.
120
+ try:
121
+ valid_id = choice(run_keyword("get_ids_from_url", url))
122
+ return id_transformer(valid_id)
123
+ except Exception as exception:
124
+ raise AssertionError(
125
+ f"Failed to get a valid id using GET on {url}"
126
+ ) from exception
127
+
128
+ response_data = response.json()
129
+ if prepared_body := response.request.body:
130
+ if isinstance(prepared_body, bytes):
131
+ send_json = _json.loads(prepared_body.decode("UTF-8"))
132
+ else:
133
+ send_json = _json.loads(prepared_body)
134
+ else:
135
+ send_json = None
136
+
137
+ # no support for retrieving an id from an array returned on a POST request
138
+ if isinstance(response_data, list):
139
+ raise NotImplementedError(
140
+ f"Unexpected response body for POST request: expected an object but "
141
+ f"received an array ({response_data})"
142
+ )
143
+
144
+ # POST on /resource_type/{id}/array_item/ will return the updated {id} resource
145
+ # instead of a newly created resource. In this case, the send_json must be
146
+ # in the array of the 'array_item' property on {id}
147
+ send_path: str = response.request.path_url
148
+ response_href: str = response_data.get("href", "")
149
+ if response_href and (send_path not in response_href) and send_json:
150
+ try:
151
+ property_to_check = send_path.replace(response_href, "")[1:]
152
+ item_list: list[dict[str, Any]] = response_data[property_to_check]
153
+ # Use the (mandatory) id to get the POSTed resource from the list
154
+ [valid_id] = [
155
+ item[id_property]
156
+ for item in item_list
157
+ if item[id_property] == send_json[id_property]
158
+ ]
159
+ except Exception as exception:
160
+ raise AssertionError(
161
+ f"Failed to get a valid id from {response_href}"
162
+ ) from exception
163
+ else:
164
+ try:
165
+ valid_id = response_data[id_property]
166
+ except KeyError:
167
+ raise AssertionError(
168
+ f"Failed to get a valid id from {response_data}"
169
+ ) from None
170
+ return id_transformer(valid_id)
171
+
172
+
173
+ def get_ids_from_url(
174
+ url: str,
175
+ get_id_property_name: GetIdPropertyNameType,
176
+ ) -> list[str]:
177
+ path: str = run_keyword("get_parameterized_path_from_url", url)
178
+ request_data: RequestData = run_keyword("get_request_data", path, "get")
179
+ response = run_keyword(
180
+ "authorized_request",
181
+ url,
182
+ "get",
183
+ request_data.get_required_params(),
184
+ request_data.get_required_headers(),
185
+ )
186
+ response.raise_for_status()
187
+ response_data: dict[str, Any] | list[dict[str, Any]] = response.json()
188
+
189
+ # determine the property name to use
190
+ mapping = get_id_property_name(path=path)
191
+ if isinstance(mapping, str):
192
+ id_property = mapping
193
+ else:
194
+ id_property, _ = mapping
195
+
196
+ if isinstance(response_data, list):
197
+ valid_ids: list[str] = [item[id_property] for item in response_data]
198
+ return valid_ids
199
+ # if the response is an object (dict), check if it's hal+json
200
+ if embedded := response_data.get("_embedded"):
201
+ # there should be 1 item in the dict that has a value that's a list
202
+ for value in embedded.values():
203
+ if isinstance(value, list):
204
+ valid_ids = [item[id_property] for item in value]
205
+ return valid_ids
206
+ if (valid_id := response_data.get(id_property)) is not None:
207
+ return [valid_id]
208
+ valid_ids = [item[id_property] for item in response_data["items"]]
209
+ return valid_ids
210
+
211
+
212
+ def substitute_path_parameters(path: str, substitution_dict: dict[str, str]) -> str:
213
+ for path_parameter, substitution_value in substitution_dict.items():
214
+ path = path.replace("{" + path_parameter + "}", str(substitution_value))
215
+ return path
@@ -0,0 +1,42 @@
1
+ """Module holding functions related to invalidation of paths and urls."""
2
+
3
+ from random import choice
4
+ from uuid import uuid4
5
+
6
+ from robot.libraries.BuiltIn import BuiltIn
7
+
8
+ from OpenApiLibCore.protocols import GetPathDtoClassType
9
+
10
+ run_keyword = BuiltIn().run_keyword
11
+
12
+
13
+ def get_invalidated_url(
14
+ valid_url: str,
15
+ path: str,
16
+ base_url: str,
17
+ get_path_dto_class: GetPathDtoClassType,
18
+ expected_status_code: int,
19
+ ) -> str:
20
+ dto_class = get_path_dto_class(path=path)
21
+ relations = dto_class.get_path_relations()
22
+ paths = [
23
+ p.invalid_value
24
+ for p in relations
25
+ if p.invalid_value_error_code == expected_status_code
26
+ ]
27
+ if paths:
28
+ url = f"{base_url}{choice(paths)}"
29
+ return url
30
+ parameterized_path: str = run_keyword("get_parameterized_path_from_url", valid_url)
31
+ parameterized_url = base_url + parameterized_path
32
+ valid_url_parts = list(reversed(valid_url.split("/")))
33
+ parameterized_parts = reversed(parameterized_url.split("/"))
34
+ for index, (parameterized_part, _) in enumerate(
35
+ zip(parameterized_parts, valid_url_parts)
36
+ ):
37
+ if parameterized_part.startswith("{") and parameterized_part.endswith("}"):
38
+ valid_url_parts[index] = uuid4().hex
39
+ valid_url_parts.reverse()
40
+ invalid_url = "/".join(valid_url_parts)
41
+ return invalid_url
42
+ raise ValueError(f"{parameterized_path} could not be invalidated.")
@@ -0,0 +1,38 @@
1
+ """A module holding Protcols."""
2
+
3
+ from typing import Callable, Protocol, Type
4
+
5
+ from openapi_core.contrib.requests import (
6
+ RequestsOpenAPIRequest,
7
+ RequestsOpenAPIResponse,
8
+ )
9
+
10
+ from OpenApiLibCore.dto_base import Dto
11
+
12
+
13
+ class ResponseValidatorType(Protocol):
14
+ def __call__(
15
+ self, request: RequestsOpenAPIRequest, response: RequestsOpenAPIResponse
16
+ ) -> None: ... # pragma: no cover
17
+
18
+
19
+ class GetDtoClassType(Protocol):
20
+ def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover
21
+
22
+ def __call__(self, path: str, method: str) -> Type[Dto]: ... # pragma: no cover
23
+
24
+
25
+ class GetIdPropertyNameType(Protocol):
26
+ def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover
27
+
28
+ def __call__(
29
+ self, path: str
30
+ ) -> tuple[
31
+ str, Callable[[str], str] | Callable[[int], int]
32
+ ]: ... # pragma: no cover
33
+
34
+
35
+ class GetPathDtoClassType(Protocol):
36
+ def __init__(self, mappings_module_name: str) -> None: ... # pragma: no cover
37
+
38
+ def __call__(self, path: str) -> Type[Dto]: ... # pragma: no cover
@@ -0,0 +1,246 @@
1
+ """Module holding the classes used to manage request data."""
2
+
3
+ from copy import deepcopy
4
+ from dataclasses import dataclass, field
5
+ from functools import cached_property
6
+ from random import sample
7
+ from typing import Any
8
+
9
+ from OpenApiLibCore.annotations import JSON
10
+ from OpenApiLibCore.dto_base import Dto
11
+ from OpenApiLibCore.dto_utils import DefaultDto
12
+ from OpenApiLibCore.models import (
13
+ ObjectSchema,
14
+ ParameterObject,
15
+ ResolvedSchemaObjectTypes,
16
+ UnionTypeSchema,
17
+ )
18
+
19
+
20
+ @dataclass
21
+ class RequestValues:
22
+ """Helper class to hold parameter values needed to make a request."""
23
+
24
+ url: str
25
+ method: str
26
+ params: dict[str, JSON] = field(default_factory=dict)
27
+ headers: dict[str, JSON] = field(default_factory=dict)
28
+ json_data: dict[str, JSON] = field(default_factory=dict)
29
+
30
+ def override_body_value(self, name: str, value: JSON) -> None:
31
+ if name in self.json_data:
32
+ self.json_data[name] = value
33
+
34
+ def override_header_value(self, name: str, value: JSON) -> None:
35
+ if name in self.headers:
36
+ self.headers[name] = value
37
+
38
+ def override_param_value(self, name: str, value: JSON) -> None:
39
+ if name in self.params:
40
+ self.params[name] = str(value)
41
+
42
+ def override_request_value(self, name: str, value: JSON) -> None:
43
+ self.override_body_value(name=name, value=value)
44
+ self.override_header_value(name=name, value=value)
45
+ self.override_param_value(name=name, value=value)
46
+
47
+ def remove_parameters(self, parameters: list[str]) -> None:
48
+ for parameter in parameters:
49
+ _ = self.params.pop(parameter, None)
50
+ _ = self.headers.pop(parameter, None)
51
+ _ = self.json_data.pop(parameter, None)
52
+
53
+
54
+ @dataclass
55
+ class RequestData:
56
+ """Helper class to manage parameters used when making requests."""
57
+
58
+ dto: Dto | DefaultDto = field(default_factory=DefaultDto)
59
+ body_schema: ObjectSchema | None = None
60
+ parameters: list[ParameterObject] = field(default_factory=list)
61
+ params: dict[str, JSON] = field(default_factory=dict)
62
+ headers: dict[str, JSON] = field(default_factory=dict)
63
+ has_body: bool = True
64
+
65
+ def __post_init__(self) -> None:
66
+ # prevent modification by reference
67
+ self.params = deepcopy(self.params)
68
+ self.headers = deepcopy(self.headers)
69
+
70
+ @property
71
+ def has_optional_properties(self) -> bool:
72
+ """Whether or not the dto data (json data) contains optional properties."""
73
+
74
+ def is_required_property(property_name: str) -> bool:
75
+ return property_name in self.required_property_names
76
+
77
+ properties = (self.dto.as_dict()).keys()
78
+ return not all(map(is_required_property, properties))
79
+
80
+ @property
81
+ def required_property_names(self) -> list[str]:
82
+ if self.body_schema:
83
+ return self.body_schema.required
84
+ return []
85
+
86
+ @property
87
+ def has_optional_params(self) -> bool:
88
+ """Whether or not any of the query parameters are optional."""
89
+
90
+ def is_optional_param(query_param: str) -> bool:
91
+ optional_params = [
92
+ p.name for p in self.parameters if p.in_ == "query" and not p.required
93
+ ]
94
+ return query_param in optional_params
95
+
96
+ return any(map(is_optional_param, self.params))
97
+
98
+ @cached_property
99
+ def params_that_can_be_invalidated(self) -> set[str]:
100
+ """
101
+ The query parameters that can be invalidated by violating data
102
+ restrictions, data type or by not providing them in a request.
103
+ """
104
+ result = set()
105
+ params = [h for h in self.parameters if h.in_ == "query"]
106
+ for param in params:
107
+ # required params can be omitted to invalidate a request
108
+ if param.required:
109
+ result.add(param.name)
110
+ continue
111
+
112
+ if param.schema_ is None:
113
+ continue
114
+
115
+ possible_schemas: list[ResolvedSchemaObjectTypes] = []
116
+ if isinstance(param.schema_, UnionTypeSchema):
117
+ possible_schemas = param.schema_.resolved_schemas
118
+ else:
119
+ possible_schemas = [param.schema_]
120
+
121
+ for param_schema in possible_schemas:
122
+ if param_schema.can_be_invalidated:
123
+ result.add(param.name)
124
+
125
+ return result
126
+
127
+ @property
128
+ def has_optional_headers(self) -> bool:
129
+ """Whether or not any of the headers are optional."""
130
+
131
+ def is_optional_header(header: str) -> bool:
132
+ optional_headers = [
133
+ p.name for p in self.parameters if p.in_ == "header" and not p.required
134
+ ]
135
+ return header in optional_headers
136
+
137
+ return any(map(is_optional_header, self.headers))
138
+
139
+ @cached_property
140
+ def headers_that_can_be_invalidated(self) -> set[str]:
141
+ """
142
+ The header parameters that can be invalidated by violating data
143
+ restrictions or by not providing them in a request.
144
+ """
145
+ result = set()
146
+ headers = [h for h in self.parameters if h.in_ == "header"]
147
+ for header in headers:
148
+ # required headers can be omitted to invalidate a request
149
+ if header.required:
150
+ result.add(header.name)
151
+ continue
152
+
153
+ if header.schema_ is None:
154
+ continue
155
+
156
+ possible_schemas: list[ResolvedSchemaObjectTypes] = []
157
+ if isinstance(header.schema_, UnionTypeSchema):
158
+ possible_schemas = header.schema_.resolved_schemas
159
+ else:
160
+ possible_schemas = [header.schema_]
161
+
162
+ for param_schema in possible_schemas:
163
+ if param_schema.can_be_invalidated:
164
+ result.add(header.name)
165
+
166
+ return result
167
+
168
+ def get_required_properties_dict(self) -> dict[str, Any]:
169
+ """Get the json-compatible dto data containing only the required properties."""
170
+ relations = self.dto.get_relations()
171
+ mandatory_properties = [
172
+ relation.property_name
173
+ for relation in relations
174
+ if getattr(relation, "treat_as_mandatory", False)
175
+ ]
176
+ required_properties = self.body_schema.required if self.body_schema else []
177
+ required_properties.extend(mandatory_properties)
178
+
179
+ required_properties_dict: dict[str, Any] = {}
180
+ for key, value in (self.dto.as_dict()).items():
181
+ if key in required_properties:
182
+ required_properties_dict[key] = value
183
+ return required_properties_dict
184
+
185
+ def get_minimal_body_dict(self) -> dict[str, Any]:
186
+ required_properties_dict = self.get_required_properties_dict()
187
+
188
+ min_properties = 0
189
+ if self.body_schema and self.body_schema.minProperties is not None:
190
+ min_properties = self.body_schema.minProperties
191
+
192
+ number_of_optional_properties_to_add = min_properties - len(
193
+ required_properties_dict
194
+ )
195
+
196
+ if number_of_optional_properties_to_add < 1:
197
+ return required_properties_dict
198
+
199
+ optional_properties_dict = {
200
+ k: v
201
+ for k, v in self.dto.as_dict().items()
202
+ if k not in required_properties_dict
203
+ }
204
+ optional_properties_to_keep = sample(
205
+ sorted(optional_properties_dict), number_of_optional_properties_to_add
206
+ )
207
+ optional_properties_dict = {
208
+ k: v
209
+ for k, v in optional_properties_dict.items()
210
+ if k in optional_properties_to_keep
211
+ }
212
+
213
+ return {**required_properties_dict, **optional_properties_dict}
214
+
215
+ def get_required_params(self) -> dict[str, JSON]:
216
+ """Get the params dict containing only the required query parameters."""
217
+ return {
218
+ k: v for k, v in self.params.items() if k in self.required_parameter_names
219
+ }
220
+
221
+ def get_required_headers(self) -> dict[str, JSON]:
222
+ """Get the headers dict containing only the required headers."""
223
+ return {
224
+ k: v for k, v in self.headers.items() if k in self.required_parameter_names
225
+ }
226
+
227
+ @property
228
+ def required_parameter_names(self) -> list[str]:
229
+ """
230
+ The names of the mandatory parameters, including the parameters configured to be
231
+ treated as mandatory using a PropertyValueConstraint.
232
+ """
233
+ relations = self.dto.get_parameter_relations()
234
+ mandatory_property_names = [
235
+ relation.property_name
236
+ for relation in relations
237
+ if getattr(relation, "treat_as_mandatory", False)
238
+ ]
239
+ parameter_names = [p.name for p in self.parameters]
240
+ mandatory_parameters = [
241
+ p for p in mandatory_property_names if p in parameter_names
242
+ ]
243
+
244
+ required_parameters = [p.name for p in self.parameters if p.required]
245
+ required_parameters.extend(mandatory_parameters)
246
+ return required_parameters
@@ -0,0 +1,55 @@
1
+ """Module holding the functions related to relations between resources."""
2
+
3
+ from requests import Response
4
+ from robot.api import logger
5
+ from robot.libraries.BuiltIn import BuiltIn
6
+
7
+ import OpenApiLibCore.path_functions as _path_functions
8
+ from OpenApiLibCore.dto_base import IdReference
9
+ from OpenApiLibCore.models import OpenApiObject
10
+ from OpenApiLibCore.request_data import RequestData
11
+
12
+ run_keyword = BuiltIn().run_keyword
13
+
14
+
15
+ def ensure_in_use(
16
+ url: str,
17
+ base_url: str,
18
+ openapi_spec: OpenApiObject,
19
+ resource_relation: IdReference,
20
+ ) -> None:
21
+ resource_id = ""
22
+
23
+ path = url.replace(base_url, "")
24
+ path_parts = path.split("/")
25
+ parameterized_path = _path_functions.get_parametrized_path(
26
+ path=path, openapi_spec=openapi_spec
27
+ )
28
+ parameterized_path_parts = parameterized_path.split("/")
29
+ for part, param_part in zip(
30
+ reversed(path_parts), reversed(parameterized_path_parts)
31
+ ):
32
+ if param_part.endswith("}"):
33
+ resource_id = part
34
+ break
35
+ if not resource_id:
36
+ raise ValueError(f"The provided url ({url}) does not contain an id.")
37
+ request_data: RequestData = run_keyword(
38
+ "get_request_data", resource_relation.post_path, "post"
39
+ )
40
+ json_data = request_data.dto.as_dict()
41
+ json_data[resource_relation.property_name] = resource_id
42
+ post_url: str = run_keyword("get_valid_url", resource_relation.post_path)
43
+ response: Response = run_keyword(
44
+ "authorized_request",
45
+ post_url,
46
+ "post",
47
+ request_data.params,
48
+ request_data.headers,
49
+ json_data,
50
+ )
51
+ if not response.ok:
52
+ logger.debug(
53
+ f"POST on {post_url} with json {json_data} failed: {response.json()}"
54
+ )
55
+ response.raise_for_status()