robotframework-openapitools 1.0.0b4__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 (30) hide show
  1. OpenApiDriver/openapi_executors.py +7 -3
  2. OpenApiDriver/openapidriver.libspec +3 -3
  3. OpenApiLibCore/__init__.py +0 -0
  4. OpenApiLibCore/data_generation/__init__.py +0 -0
  5. OpenApiLibCore/data_generation/body_data_generation.py +4 -4
  6. OpenApiLibCore/data_generation/data_generation_core.py +18 -45
  7. OpenApiLibCore/data_invalidation.py +1 -5
  8. OpenApiLibCore/dto_base.py +31 -22
  9. OpenApiLibCore/dto_utils.py +31 -3
  10. OpenApiLibCore/localized_faker.py +0 -0
  11. OpenApiLibCore/models.py +26 -18
  12. OpenApiLibCore/openapi_libcore.libspec +26 -26
  13. OpenApiLibCore/openapi_libcore.py +35 -26
  14. OpenApiLibCore/parameter_utils.py +3 -3
  15. OpenApiLibCore/path_functions.py +5 -6
  16. OpenApiLibCore/path_invalidation.py +5 -7
  17. OpenApiLibCore/protocols.py +6 -0
  18. OpenApiLibCore/request_data.py +0 -0
  19. OpenApiLibCore/resource_relations.py +4 -2
  20. OpenApiLibCore/validation.py +4 -9
  21. OpenApiLibCore/value_utils.py +1 -1
  22. openapi_libgen/generator.py +2 -2
  23. openapi_libgen/parsing_utils.py +9 -5
  24. openapi_libgen/spec_parser.py +4 -4
  25. {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/METADATA +1 -1
  26. robotframework_openapitools-1.0.0b5.dist-info/RECORD +40 -0
  27. robotframework_openapitools-1.0.0b4.dist-info/RECORD +0 -40
  28. {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/LICENSE +0 -0
  29. {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/WHEEL +0 -0
  30. {robotframework_openapitools-1.0.0b4.dist-info → robotframework_openapitools-1.0.0b5.dist-info}/entry_points.txt +0 -0
@@ -133,11 +133,15 @@ class OpenApiExecutors(OpenApiLibCore):
133
133
  """
134
134
  valid_url: str = run_keyword("get_valid_url", path)
135
135
 
136
- if not (
137
- url := run_keyword(
136
+ try:
137
+ url = run_keyword(
138
138
  "get_invalidated_url", valid_url, path, expected_status_code
139
139
  )
140
- ):
140
+ except Exception as exception:
141
+ message = getattr(exception, "message", "")
142
+ if not message.startswith("ValueError"):
143
+ raise exception # pragma: no cover
144
+
141
145
  raise SkipExecution(
142
146
  f"Path {path} does not contain resource references that "
143
147
  f"can be invalidated."
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2025-05-11T17:23:01+00:00" specversion="6" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapidriver.py" lineno="358">
3
- <version>1.0.0b3</version>
2
+ <keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2025-06-09T18:38:35+00:00" specversion="6" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapidriver.py" lineno="358">
3
+ <version>1.0.0b5</version>
4
4
  <doc>&lt;p&gt;Visit the &lt;a href="https://github.com/MarketSquare/robotframework-openapidriver"&gt;library page&lt;/a&gt; for an introduction and examples.&lt;/p&gt;</doc>
5
5
  <tags>
6
6
  </tags>
@@ -247,7 +247,7 @@
247
247
  </init>
248
248
  </inits>
249
249
  <keywords>
250
- <kw name="Test Endpoint" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="162">
250
+ <kw name="Test Endpoint" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="166">
251
251
  <arguments repr="path: str, method: str, status_code: int">
252
252
  <arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
253
253
  <name>path</name>
File without changes
File without changes
@@ -8,7 +8,7 @@ from typing import Any
8
8
 
9
9
  from robot.api import logger
10
10
 
11
- import OpenApiLibCore.path_functions as pf
11
+ import OpenApiLibCore.path_functions as _path_functions
12
12
  from OpenApiLibCore.annotations import JSON
13
13
  from OpenApiLibCore.dto_base import (
14
14
  Dto,
@@ -72,7 +72,7 @@ def get_dict_data_for_dto_class(
72
72
  property_names = get_property_names_to_process(schema=schema, dto_class=dto_class)
73
73
 
74
74
  for property_name in property_names:
75
- property_schema = schema.properties.root[property_name]
75
+ property_schema = schema.properties.root[property_name] # type: ignore[union-attr]
76
76
  if property_schema.readOnly:
77
77
  continue
78
78
 
@@ -178,7 +178,7 @@ def get_property_names_to_process(
178
178
  ) -> list[str]:
179
179
  property_names = []
180
180
 
181
- for property_name in schema.properties.root:
181
+ for property_name in schema.properties.root: # type: ignore[union-attr]
182
182
  # register the oas_name
183
183
  _ = get_safe_name_for_oas_name(property_name)
184
184
  if constrained_values := get_constrained_values(
@@ -243,7 +243,7 @@ def get_dependent_id(
243
243
  except ValueError:
244
244
  return None
245
245
 
246
- valid_id = pf.get_valid_id_for_path(
246
+ valid_id = _path_functions.get_valid_id_for_path(
247
247
  path=id_get_path, get_id_property_name=get_id_property_name
248
248
  )
249
249
  logger.debug(f"get_dependent_id for {id_get_path} returned {valid_id}")
@@ -3,14 +3,13 @@ Module holding the main functions related to data generation
3
3
  for the requests made as part of keyword exection.
4
4
  """
5
5
 
6
- import re
7
6
  from dataclasses import Field, field, make_dataclass
8
7
  from random import choice
9
8
  from typing import Any, cast
10
9
 
11
10
  from robot.api import logger
12
11
 
13
- import OpenApiLibCore.path_functions as pf
12
+ import OpenApiLibCore.path_functions as _path_functions
14
13
  from OpenApiLibCore.annotations import JSON
15
14
  from OpenApiLibCore.dto_base import (
16
15
  Dto,
@@ -23,7 +22,6 @@ from OpenApiLibCore.models import (
23
22
  OpenApiObject,
24
23
  OperationObject,
25
24
  ParameterObject,
26
- RequestBodyObject,
27
25
  UnionTypeSchema,
28
26
  )
29
27
  from OpenApiLibCore.parameter_utils import get_safe_name_for_oas_name
@@ -47,11 +45,13 @@ def get_request_data(
47
45
  dto_cls_name = get_dto_cls_name(path=path, method=method)
48
46
  # The path can contain already resolved Ids that have to be matched
49
47
  # against the parametrized paths in the paths section.
50
- spec_path = pf.get_parametrized_path(path=path, openapi_spec=openapi_spec)
48
+ spec_path = _path_functions.get_parametrized_path(
49
+ path=path, openapi_spec=openapi_spec
50
+ )
51
51
  dto_class = get_dto_class(path=spec_path, method=method)
52
52
  try:
53
53
  path_item = openapi_spec.paths[spec_path]
54
- operation_spec = getattr(path_item, method)
54
+ operation_spec: OperationObject | None = getattr(path_item, method)
55
55
  if operation_spec is None:
56
56
  raise AttributeError
57
57
  except AttributeError:
@@ -77,38 +77,30 @@ def get_request_data(
77
77
  has_body=False,
78
78
  )
79
79
 
80
- headers.update({"content-type": get_content_type(operation_spec.requestBody)})
81
-
82
- body_schema = operation_spec.requestBody
83
- media_type_dict = body_schema.content
84
- supported_types = [v for k, v in media_type_dict.items() if "json" in k]
85
- supported_schemas = [t.schema_ for t in supported_types if t.schema_ is not None]
80
+ body_schema = operation_spec.requestBody.schema_
86
81
 
87
- if not supported_schemas:
88
- raise ValueError(f"No supported content schema found: {media_type_dict}")
89
-
90
- if len(supported_schemas) > 1:
91
- logger.warn(
92
- f"Multiple JSON media types defined for requestBody, using the first candidate {media_type_dict}"
82
+ if not body_schema:
83
+ raise ValueError(
84
+ f"No supported content schema found: {operation_spec.requestBody.content}"
93
85
  )
94
86
 
95
- schema = supported_schemas[0]
87
+ headers.update({"content-type": operation_spec.requestBody.mime_type})
96
88
 
97
- if isinstance(schema, UnionTypeSchema):
98
- resolved_schemas = schema.resolved_schemas
99
- schema = choice(resolved_schemas)
89
+ if isinstance(body_schema, UnionTypeSchema):
90
+ resolved_schemas = body_schema.resolved_schemas
91
+ body_schema = choice(resolved_schemas)
100
92
 
101
- if not isinstance(schema, ObjectSchema):
102
- raise ValueError(f"Selected schema is not an object schema: {schema}")
93
+ if not isinstance(body_schema, ObjectSchema):
94
+ raise ValueError(f"Selected schema is not an object schema: {body_schema}")
103
95
 
104
96
  dto_data = _get_json_data_for_dto_class(
105
- schema=schema,
97
+ schema=body_schema,
106
98
  dto_class=dto_class,
107
99
  get_id_property_name=get_id_property_name,
108
100
  operation_id=operation_spec.operationId,
109
101
  )
110
102
  dto_instance = _get_dto_instance_from_dto_data(
111
- object_schema=schema,
103
+ object_schema=body_schema,
112
104
  dto_class=dto_class,
113
105
  dto_data=dto_data,
114
106
  method_spec=operation_spec,
@@ -116,7 +108,7 @@ def get_request_data(
116
108
  )
117
109
  return RequestData(
118
110
  dto=dto_instance,
119
- body_schema=schema,
111
+ body_schema=body_schema,
120
112
  parameters=parameters,
121
113
  params=params,
122
114
  headers=headers,
@@ -198,25 +190,6 @@ def get_dto_cls_name(path: str, method: str) -> str:
198
190
  return result
199
191
 
200
192
 
201
- def get_content_type(body_spec: RequestBodyObject) -> str:
202
- """Get and validate the first supported content type from the requested body spec
203
-
204
- Should be application/json like content type,
205
- e.g "application/json;charset=utf-8" or "application/merge-patch+json"
206
- """
207
- content_types: list[str] = list(body_spec.content.keys())
208
- json_regex = r"application/([a-z\-]+\+)?json(;\s?charset=(.+))?"
209
- for content_type in content_types:
210
- if re.search(json_regex, content_type):
211
- return content_type
212
-
213
- # At present no supported for other types.
214
- raise NotImplementedError(
215
- f"Only content types like 'application/json' are supported. "
216
- f"Content types definded in the spec are '{content_types}'."
217
- )
218
-
219
-
220
193
  def get_request_parameters(
221
194
  dto_class: Dto | type[Dto], method_spec: OperationObject
222
195
  ) -> tuple[list[ParameterObject], dict[str, Any], dict[str, str]]:
@@ -16,7 +16,6 @@ from OpenApiLibCore.dto_base import (
16
16
  NOT_SET,
17
17
  Dto,
18
18
  IdReference,
19
- PathPropertiesConstraint,
20
19
  PropertyValueConstraint,
21
20
  UniquePropertyValueConstraint,
22
21
  )
@@ -35,10 +34,7 @@ def get_invalid_body_data(
35
34
  invalid_property_default_response: int,
36
35
  ) -> dict[str, Any]:
37
36
  method = method.lower()
38
- data_relations = request_data.dto.get_relations_for_error_code(status_code)
39
- data_relations = [
40
- r for r in data_relations if not isinstance(r, PathPropertiesConstraint)
41
- ]
37
+ data_relations = request_data.dto.get_body_relations_for_error_code(status_code)
42
38
  if not data_relations:
43
39
  if request_data.body_schema is None:
44
40
  raise ValueError(
@@ -83,17 +83,17 @@ class Dto(ABC):
83
83
  """Base class for the Dto class."""
84
84
 
85
85
  @staticmethod
86
- def get_parameter_relations() -> list[ResourceRelation]:
86
+ def get_path_relations() -> list[PathPropertiesConstraint]:
87
87
  """Return the list of Relations for the header and query parameters."""
88
88
  return []
89
89
 
90
- def get_parameter_relations_for_error_code(
90
+ def get_path_relations_for_error_code(
91
91
  self, error_code: int
92
- ) -> list[ResourceRelation]:
92
+ ) -> list[PathPropertiesConstraint]:
93
93
  """Return the list of Relations associated with the given error_code."""
94
- relations: list[ResourceRelation] = [
94
+ relations: list[PathPropertiesConstraint] = [
95
95
  r
96
- for r in self.get_parameter_relations()
96
+ for r in self.get_path_relations()
97
97
  if r.error_code == error_code
98
98
  or (
99
99
  getattr(r, "invalid_value_error_code", None) == error_code
@@ -103,15 +103,17 @@ class Dto(ABC):
103
103
  return relations
104
104
 
105
105
  @staticmethod
106
- def get_relations() -> list[ResourceRelation]:
107
- """Return the list of Relations for the (json) body."""
106
+ def get_parameter_relations() -> list[ResourceRelation]:
107
+ """Return the list of Relations for the header and query parameters."""
108
108
  return []
109
109
 
110
- def get_relations_for_error_code(self, error_code: int) -> list[ResourceRelation]:
110
+ def get_parameter_relations_for_error_code(
111
+ self, error_code: int
112
+ ) -> list[ResourceRelation]:
111
113
  """Return the list of Relations associated with the given error_code."""
112
114
  relations: list[ResourceRelation] = [
113
115
  r
114
- for r in self.get_relations()
116
+ for r in self.get_parameter_relations()
115
117
  if r.error_code == error_code
116
118
  or (
117
119
  getattr(r, "invalid_value_error_code", None) == error_code
@@ -120,6 +122,11 @@ class Dto(ABC):
120
122
  ]
121
123
  return relations
122
124
 
125
+ @staticmethod
126
+ def get_relations() -> list[ResourceRelation]:
127
+ """Return the list of Relations for the (json) body."""
128
+ return []
129
+
123
130
  def get_body_relations_for_error_code(
124
131
  self, error_code: int
125
132
  ) -> list[ResourceRelation]:
@@ -127,8 +134,16 @@ class Dto(ABC):
127
134
  Return the list of Relations associated with the given error_code that are
128
135
  applicable to the body / payload of the request.
129
136
  """
130
- all_relations = self.get_relations_for_error_code(error_code=error_code)
131
- return [r for r in all_relations if not isinstance(r, PathPropertiesConstraint)]
137
+ relations: list[ResourceRelation] = [
138
+ r
139
+ for r in self.get_relations()
140
+ if r.error_code == error_code
141
+ or (
142
+ getattr(r, "invalid_value_error_code", None) == error_code
143
+ and getattr(r, "invalid_value", None) != NOT_SET
144
+ )
145
+ ]
146
+ return relations
132
147
 
133
148
  def get_invalidated_data(
134
149
  self,
@@ -139,17 +154,11 @@ class Dto(ABC):
139
154
  """Return a data set with one of the properties set to an invalid value or type."""
140
155
  properties: dict[str, Any] = self.as_dict()
141
156
 
142
- # schema = resolve_schema(schema)
143
-
144
- relations = self.get_relations_for_error_code(error_code=status_code)
145
- # filter PathProperyConstraints since in that case no data can be invalidated
146
- relations = [
147
- r for r in relations if not isinstance(r, PathPropertiesConstraint)
148
- ]
157
+ relations = self.get_body_relations_for_error_code(error_code=status_code)
149
158
  property_names = [r.property_name for r in relations]
150
159
  if status_code == invalid_property_default_code:
151
160
  # add all properties defined in the schema, including optional properties
152
- property_names.extend((schema.properties.root.keys()))
161
+ property_names.extend((schema.properties.root.keys())) # type: ignore[union-attr]
153
162
  if not property_names:
154
163
  raise ValueError(
155
164
  f"No property can be invalidated to cause status_code {status_code}"
@@ -167,8 +176,8 @@ class Dto(ABC):
167
176
  if id_dependencies:
168
177
  invalid_id = uuid4().hex
169
178
  logger.debug(
170
- f"Breaking IdDependency for status_code {status_code}: replacing "
171
- f"{properties[property_name]} with {invalid_id}"
179
+ f"Breaking IdDependency for status_code {status_code}: setting "
180
+ f"{property_name} to {invalid_id}"
172
181
  )
173
182
  properties[property_name] = invalid_id
174
183
  return properties
@@ -191,7 +200,7 @@ class Dto(ABC):
191
200
  )
192
201
  return properties
193
202
 
194
- value_schema = schema.properties.root[property_name]
203
+ value_schema = schema.properties.root[property_name] # type: ignore[union-attr]
195
204
  if isinstance(value_schema, UnionTypeSchema):
196
205
  # Filter "type": "null" from the possible types since this indicates an
197
206
  # optional / nullable property that can only be invalidated by sending
@@ -7,7 +7,11 @@ from typing import Any, Callable, Type, overload
7
7
  from robot.api import logger
8
8
 
9
9
  from OpenApiLibCore.dto_base import Dto
10
- from OpenApiLibCore.protocols import GetDtoClassType, GetIdPropertyNameType
10
+ from OpenApiLibCore.protocols import (
11
+ GetDtoClassType,
12
+ GetIdPropertyNameType,
13
+ GetPathDtoClassType,
14
+ )
11
15
 
12
16
 
13
17
  @dataclass
@@ -49,6 +53,30 @@ class GetDtoClass:
49
53
  return DefaultDto
50
54
 
51
55
 
56
+ def get_path_dto_class(mappings_module_name: str) -> GetPathDtoClassType:
57
+ return GetPathDtoClass(mappings_module_name=mappings_module_name)
58
+
59
+
60
+ class GetPathDtoClass:
61
+ """Callable class to return Dtos from user-implemented mappings file."""
62
+
63
+ def __init__(self, mappings_module_name: str) -> None:
64
+ try:
65
+ mappings_module = import_module(mappings_module_name)
66
+ self.dto_mapping: dict[str, Type[Dto]] = mappings_module.PATH_MAPPING
67
+ except (ImportError, AttributeError, ValueError) as exception:
68
+ if mappings_module_name != "no mapping":
69
+ logger.error(f"PATH_MAPPING was not imported: {exception}")
70
+ self.dto_mapping = {}
71
+
72
+ def __call__(self, path: str) -> Type[Dto]:
73
+ try:
74
+ return self.dto_mapping[path]
75
+ except KeyError:
76
+ logger.debug(f"No Dto mapping for {path}.")
77
+ return DefaultDto
78
+
79
+
52
80
  def get_id_property_name(mappings_module_name: str) -> GetIdPropertyNameType:
53
81
  return GetIdPropertyName(mappings_module_name=mappings_module_name)
54
82
 
@@ -86,11 +114,11 @@ class GetIdPropertyName:
86
114
 
87
115
 
88
116
  @overload
89
- def dummy_transformer(valid_id: str) -> str: ...
117
+ def dummy_transformer(valid_id: str) -> str: ... # pragma: no cover
90
118
 
91
119
 
92
120
  @overload
93
- def dummy_transformer(valid_id: int) -> int: ...
121
+ def dummy_transformer(valid_id: int) -> int: ... # pragma: no cover
94
122
 
95
123
 
96
124
  def dummy_transformer(valid_id: Any) -> Any:
File without changes
OpenApiLibCore/models.py CHANGED
@@ -234,7 +234,7 @@ class IntegerSchema(SchemaBase[int], frozen=True):
234
234
 
235
235
  return randint(self._min_value, self._max_value)
236
236
 
237
- def get_values_out_of_bounds(self, current_value: int) -> list[int]:
237
+ def get_values_out_of_bounds(self, current_value: int) -> list[int]: # pylint: disable=unused-argument
238
238
  invalid_values: list[int] = []
239
239
 
240
240
  if self._min_value > self._min_int:
@@ -333,7 +333,7 @@ class NumberSchema(SchemaBase[float], frozen=True):
333
333
 
334
334
  return uniform(self._min_value, self._max_value)
335
335
 
336
- def get_values_out_of_bounds(self, current_value: float) -> list[float]:
336
+ def get_values_out_of_bounds(self, current_value: float) -> list[float]: # pylint: disable=unused-argument
337
337
  invalid_values: list[float] = []
338
338
 
339
339
  if self._min_value > self._min_float:
@@ -643,21 +643,33 @@ class RequestBodyObject(BaseModel):
643
643
  required: bool = False
644
644
  description: str = ""
645
645
 
646
- @property
646
+ @cached_property
647
647
  def schema_(self) -> SchemaObjectTypes | None:
648
- schemas = [
649
- media_type.schema_
650
- for mime_type, media_type in self.content.items()
651
- if "json" in mime_type
652
- ]
653
- if None in schemas:
654
- schemas.remove(None)
655
- if not schemas:
648
+ if not self.mime_type:
649
+ return None
650
+
651
+ if len(self._json_schemas) > 1:
652
+ logger.info(
653
+ f"Multiple JSON media types defined for requestBody, "
654
+ f"using the first candidate from {self.content}"
655
+ )
656
+ return self._json_schemas[self.mime_type]
657
+
658
+ @cached_property
659
+ def mime_type(self) -> str | None:
660
+ if not self._json_schemas:
656
661
  return None
657
662
 
658
- if len(schemas) > 1:
659
- logger.warn(f"Multiple schemas defined for request body: {self.content}")
660
- return schemas.pop()
663
+ return next(iter(self._json_schemas))
664
+
665
+ @cached_property
666
+ def _json_schemas(self) -> dict[str, SchemaObjectTypes]:
667
+ json_schemas = {
668
+ mime_type: media_type.schema_
669
+ for mime_type, media_type in self.content.items()
670
+ if "json" in mime_type and media_type.schema_ is not None
671
+ }
672
+ return json_schemas
661
673
 
662
674
 
663
675
  class HeaderObject(BaseModel): ...
@@ -673,10 +685,6 @@ class ResponseObject(BaseModel):
673
685
  links: dict[str, LinkObject] = {}
674
686
 
675
687
 
676
- # class ComponentsObject(BaseModel):
677
- # schemas: dict[str, SchemaObjectTypes]
678
-
679
-
680
688
  class OperationObject(BaseModel):
681
689
  operationId: str | None = None
682
690
  summary: str = ""