robotframework-openapitools 0.2.0__py3-none-any.whl → 0.2.2__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.
@@ -1,28 +1,18 @@
1
1
  """Module containing the classes to perform automatic OpenAPI contract validation."""
2
2
 
3
- import json as _json
4
- from enum import Enum
5
3
  from logging import getLogger
6
4
  from pathlib import Path
7
5
  from random import choice
8
6
  from typing import Any, Dict, List, Optional, Tuple, Union
9
7
 
10
- from openapi_core.contrib.requests import (
11
- RequestsOpenAPIRequest,
12
- RequestsOpenAPIResponse,
13
- )
14
- from openapi_core.exceptions import OpenAPIError
15
- from openapi_core.validation.exceptions import ValidationError
16
- from openapi_core.validation.response.exceptions import InvalidData
17
- from openapi_core.validation.schemas.exceptions import InvalidSchemaValue
18
8
  from requests import Response
19
9
  from requests.auth import AuthBase
20
10
  from requests.cookies import RequestsCookieJar as CookieJar
21
- from robot.api import Failure, SkipExecution
11
+ from robot.api import SkipExecution
22
12
  from robot.api.deco import keyword, library
23
13
  from robot.libraries.BuiltIn import BuiltIn
24
14
 
25
- from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, resolve_schema
15
+ from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, ValidationLevel
26
16
 
27
17
  run_keyword = BuiltIn().run_keyword
28
18
 
@@ -30,15 +20,6 @@ run_keyword = BuiltIn().run_keyword
30
20
  logger = getLogger(__name__)
31
21
 
32
22
 
33
- class ValidationLevel(str, Enum):
34
- """The available levels for the response_validation parameter."""
35
-
36
- DISABLED = "DISABLED"
37
- INFO = "INFO"
38
- WARN = "WARN"
39
- STRICT = "STRICT"
40
-
41
-
42
23
  @library(scope="TEST SUITE", doc_format="ROBOT")
43
24
  class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-attributes
44
25
  """Main class providing the keywords and core logic to perform endpoint validations."""
@@ -314,451 +295,3 @@ class OpenApiExecutors(OpenApiLibCore): # pylint: disable=too-many-instance-att
314
295
  if response.ok:
315
296
  original_data = response.json()
316
297
  return original_data
317
-
318
- @keyword
319
- def perform_validated_request(
320
- self,
321
- path: str,
322
- status_code: int,
323
- request_values: RequestValues,
324
- original_data: Optional[Dict[str, Any]] = None,
325
- ) -> None:
326
- """
327
- This keyword first calls the Authorized Request keyword, then the Validate
328
- Response keyword and finally validates, for `DELETE` operations, whether
329
- the target resource was indeed deleted (OK response) or not (error responses).
330
- """
331
- response = run_keyword(
332
- "authorized_request",
333
- request_values.url,
334
- request_values.method,
335
- request_values.params,
336
- request_values.headers,
337
- request_values.json_data,
338
- )
339
- if response.status_code != status_code:
340
- try:
341
- response_json = response.json()
342
- except Exception as _: # pylint: disable=broad-except
343
- logger.info(
344
- f"Failed to get json content from response. "
345
- f"Response text was: {response.text}"
346
- )
347
- response_json = {}
348
- if not response.ok:
349
- if description := response_json.get("detail"):
350
- pass
351
- else:
352
- description = response_json.get(
353
- "message", "response contains no message or detail."
354
- )
355
- logger.error(f"{response.reason}: {description}")
356
-
357
- logger.debug(
358
- f"\nSend: {_json.dumps(request_values.json_data, indent=4, sort_keys=True)}"
359
- f"\nGot: {_json.dumps(response_json, indent=4, sort_keys=True)}"
360
- )
361
- raise AssertionError(
362
- f"Response status_code {response.status_code} was not {status_code}"
363
- )
364
-
365
- run_keyword("validate_response", path, response, original_data)
366
-
367
- if request_values.method == "DELETE":
368
- get_request_data = self.get_request_data(endpoint=path, method="GET")
369
- get_params = get_request_data.params
370
- get_headers = get_request_data.headers
371
- get_response = run_keyword(
372
- "authorized_request", request_values.url, "GET", get_params, get_headers
373
- )
374
- if response.ok:
375
- if get_response.ok:
376
- raise AssertionError(
377
- f"Resource still exists after deletion. Url was {request_values.url}"
378
- )
379
- # if the path supports GET, 404 is expected, if not 405 is expected
380
- if get_response.status_code not in [404, 405]:
381
- logger.warning(
382
- f"Unexpected response after deleting resource: Status_code "
383
- f"{get_response.status_code} was received after trying to get {request_values.url} "
384
- f"after sucessfully deleting it."
385
- )
386
- elif not get_response.ok:
387
- raise AssertionError(
388
- f"Resource could not be retrieved after failed deletion. "
389
- f"Url was {request_values.url}, status_code was {get_response.status_code}"
390
- )
391
-
392
- @keyword
393
- def validate_response(
394
- self,
395
- path: str,
396
- response: Response,
397
- original_data: Optional[Dict[str, Any]] = None,
398
- ) -> None:
399
- """
400
- Validate the `response` by performing the following validations:
401
- - validate the `response` against the openapi schema for the `endpoint`
402
- - validate that the response does not contain extra properties
403
- - validate that a href, if present, refers to the correct resource
404
- - validate that the value for a property that is in the response is equal to
405
- the property value that was send
406
- - validate that no `original_data` is preserved when performing a PUT operation
407
- - validate that a PATCH operation only updates the provided properties
408
- """
409
- if response.status_code == 204:
410
- assert not response.content
411
- return None
412
-
413
- try:
414
- self._validate_response_against_spec(response)
415
- except OpenAPIError:
416
- raise Failure("Response did not pass schema validation.")
417
-
418
- request_method = response.request.method
419
- if request_method is None:
420
- logger.warning(
421
- f"Could not validate response for path {path}; no method found "
422
- f"on the request property of the provided response."
423
- )
424
- return None
425
-
426
- response_spec = self._get_response_spec(
427
- path=path,
428
- method=request_method,
429
- status_code=response.status_code,
430
- )
431
-
432
- content_type_from_response = response.headers.get("Content-Type", "unknown")
433
- mime_type_from_response, _, _ = content_type_from_response.partition(";")
434
-
435
- if not response_spec.get("content"):
436
- logger.warning(
437
- "The response cannot be validated: 'content' not specified in the OAS."
438
- )
439
- return None
440
-
441
- # multiple content types can be specified in the OAS
442
- content_types = list(response_spec["content"].keys())
443
- supported_types = [
444
- ct for ct in content_types if ct.partition(";")[0].endswith("json")
445
- ]
446
- if not supported_types:
447
- raise NotImplementedError(
448
- f"The content_types '{content_types}' are not supported. "
449
- f"Only json types are currently supported."
450
- )
451
- content_type = supported_types[0]
452
- mime_type = content_type.partition(";")[0]
453
-
454
- if mime_type != mime_type_from_response:
455
- raise ValueError(
456
- f"Content-Type '{content_type_from_response}' of the response "
457
- f"does not match '{mime_type}' as specified in the OpenAPI document."
458
- )
459
-
460
- json_response = response.json()
461
- response_schema = resolve_schema(
462
- response_spec["content"][content_type]["schema"]
463
- )
464
- if list_item_schema := response_schema.get("items"):
465
- if not isinstance(json_response, list):
466
- raise AssertionError(
467
- f"Response schema violation: the schema specifies an array as "
468
- f"response type but the response was of type {type(json_response)}."
469
- )
470
- type_of_list_items = list_item_schema.get("type")
471
- if type_of_list_items == "object":
472
- for resource in json_response:
473
- run_keyword(
474
- "validate_resource_properties", resource, list_item_schema
475
- )
476
- else:
477
- for item in json_response:
478
- self._validate_value_type(
479
- value=item, expected_type=type_of_list_items
480
- )
481
- # no further validation; value validation of individual resources should
482
- # be performed on the endpoints for the specific resource
483
- return None
484
-
485
- run_keyword("validate_resource_properties", json_response, response_schema)
486
- # ensure the href is valid if present in the response
487
- if href := json_response.get("href"):
488
- self._assert_href_is_valid(href, json_response)
489
- # every property that was sucessfully send and that is in the response
490
- # schema must have the value that was send
491
- if response.ok and response.request.method in ["POST", "PUT", "PATCH"]:
492
- run_keyword("validate_send_response", response, original_data)
493
- return None
494
-
495
- def _assert_href_is_valid(self, href: str, json_response: Dict[str, Any]) -> None:
496
- url = f"{self.origin}{href}"
497
- path = url.replace(self.base_url, "")
498
- request_data = self.get_request_data(endpoint=path, method="GET")
499
- params = request_data.params
500
- headers = request_data.headers
501
- get_response = run_keyword("authorized_request", url, "GET", params, headers)
502
- assert (
503
- get_response.json() == json_response
504
- ), f"{get_response.json()} not equal to original {json_response}"
505
-
506
- def _validate_response_against_spec(self, response: Response) -> None:
507
- try:
508
- self.validate_response_vs_spec(
509
- request=RequestsOpenAPIRequest(response.request),
510
- response=RequestsOpenAPIResponse(response),
511
- )
512
- except InvalidData as exception:
513
- errors: List[InvalidSchemaValue] = exception.__cause__
514
- validation_errors: Optional[List[ValidationError]] = getattr(
515
- errors, "schema_errors", None
516
- )
517
- if validation_errors:
518
- error_message = "\n".join(
519
- [
520
- f"{list(error.schema_path)}: {error.message}"
521
- for error in validation_errors
522
- ]
523
- )
524
- else:
525
- error_message = str(exception)
526
-
527
- if response.status_code == self.invalid_property_default_response:
528
- logger.debug(error_message)
529
- return
530
- if self.response_validation == ValidationLevel.STRICT:
531
- logger.error(error_message)
532
- raise exception
533
- if self.response_validation == ValidationLevel.WARN:
534
- logger.warning(error_message)
535
- elif self.response_validation == ValidationLevel.INFO:
536
- logger.info(error_message)
537
-
538
- @keyword
539
- def validate_resource_properties(
540
- self, resource: Dict[str, Any], schema: Dict[str, Any]
541
- ) -> None:
542
- """
543
- Validate that the `resource` does not contain any properties that are not
544
- defined in the `schema_properties`.
545
- """
546
- schema_properties = schema.get("properties", {})
547
- property_names_from_schema = set(schema_properties.keys())
548
- property_names_in_resource = set(resource.keys())
549
-
550
- if property_names_from_schema != property_names_in_resource:
551
- # The additionalProperties property determines whether properties with
552
- # unspecified names are allowed. This property can be boolean or an object
553
- # (dict) that specifies the type of any additional properties.
554
- additional_properties = schema.get("additionalProperties", True)
555
- if isinstance(additional_properties, bool):
556
- allow_additional_properties = additional_properties
557
- allowed_additional_properties_type = None
558
- else:
559
- allow_additional_properties = True
560
- allowed_additional_properties_type = additional_properties["type"]
561
-
562
- extra_property_names = property_names_in_resource.difference(
563
- property_names_from_schema
564
- )
565
- if allow_additional_properties:
566
- # If a type is defined for extra properties, validate them
567
- if allowed_additional_properties_type:
568
- extra_properties = {
569
- key: value
570
- for key, value in resource.items()
571
- if key in extra_property_names
572
- }
573
- self._validate_type_of_extra_properties(
574
- extra_properties=extra_properties,
575
- expected_type=allowed_additional_properties_type,
576
- )
577
- # If allowed, validation should not fail on extra properties
578
- extra_property_names = set()
579
-
580
- required_properties = set(schema.get("required", []))
581
- missing_properties = required_properties.difference(
582
- property_names_in_resource
583
- )
584
-
585
- if extra_property_names or missing_properties:
586
- extra = (
587
- f"\n\tExtra properties in response: {extra_property_names}"
588
- if extra_property_names
589
- else ""
590
- )
591
- missing = (
592
- f"\n\tRequired properties missing in response: {missing_properties}"
593
- if missing_properties
594
- else ""
595
- )
596
- raise AssertionError(
597
- f"Response schema violation: the response contains properties that are "
598
- f"not specified in the schema or does not contain properties that are "
599
- f"required according to the schema."
600
- f"\n\tReceived in the response: {property_names_in_resource}"
601
- f"\n\tDefined in the schema: {property_names_from_schema}"
602
- f"{extra}{missing}"
603
- )
604
-
605
- @staticmethod
606
- def _validate_value_type(value: Any, expected_type: str) -> None:
607
- type_mapping = {
608
- "string": str,
609
- "number": float,
610
- "integer": int,
611
- "boolean": bool,
612
- "array": list,
613
- "object": dict,
614
- }
615
- python_type = type_mapping.get(expected_type, None)
616
- if python_type is None:
617
- raise AssertionError(
618
- f"Validation of type '{expected_type}' is not supported."
619
- )
620
- if not isinstance(value, python_type):
621
- raise AssertionError(f"{value} is not of type {expected_type}")
622
-
623
- @staticmethod
624
- def _validate_type_of_extra_properties(
625
- extra_properties: Dict[str, Any], expected_type: str
626
- ) -> None:
627
- type_mapping = {
628
- "string": str,
629
- "number": float,
630
- "integer": int,
631
- "boolean": bool,
632
- "array": list,
633
- "object": dict,
634
- }
635
-
636
- python_type = type_mapping.get(expected_type, None)
637
- if python_type is None:
638
- logger.warning(
639
- f"Additonal properties were not validated: "
640
- f"type '{expected_type}' is not supported."
641
- )
642
- return
643
-
644
- invalid_extra_properties = {
645
- key: value
646
- for key, value in extra_properties.items()
647
- if not isinstance(value, python_type)
648
- }
649
- if invalid_extra_properties:
650
- raise AssertionError(
651
- f"Response contains invalid additionalProperties: "
652
- f"{invalid_extra_properties} are not of type {expected_type}."
653
- )
654
-
655
- @staticmethod
656
- @keyword
657
- def validate_send_response(
658
- response: Response, original_data: Optional[Dict[str, Any]] = None
659
- ) -> None:
660
- """
661
- Validate that each property that was send that is in the response has the value
662
- that was send.
663
- In case a PATCH request, validate that only the properties that were patched
664
- have changed and that other properties are still at their pre-patch values.
665
- """
666
-
667
- def validate_list_response(
668
- send_list: List[Any], received_list: List[Any]
669
- ) -> None:
670
- for item in send_list:
671
- if item not in received_list:
672
- raise AssertionError(
673
- f"Received value '{received_list}' does "
674
- f"not contain '{item}' in the {response.request.method} request."
675
- f"\nSend: {_json.dumps(send_json, indent=4, sort_keys=True)}"
676
- f"\nGot: {_json.dumps(response_data, indent=4, sort_keys=True)}"
677
- )
678
-
679
- def validate_dict_response(
680
- send_dict: Dict[str, Any], received_dict: Dict[str, Any]
681
- ) -> None:
682
- for send_property_name, send_property_value in send_dict.items():
683
- # sometimes, a property in the request is not in the response, e.g. a password
684
- if send_property_name not in received_dict.keys():
685
- continue
686
- if send_property_value is not None:
687
- # if a None value is send, the target property should be cleared or
688
- # reverted to the default value (which cannot be specified in the
689
- # openapi document)
690
- received_value = received_dict[send_property_name]
691
- # In case of lists / arrays, the send values are often appended to
692
- # existing data
693
- if isinstance(received_value, list):
694
- validate_list_response(
695
- send_list=send_property_value, received_list=received_value
696
- )
697
- continue
698
-
699
- # when dealing with objects, we'll need to iterate the properties
700
- if isinstance(received_value, dict):
701
- validate_dict_response(
702
- send_dict=send_property_value, received_dict=received_value
703
- )
704
- continue
705
-
706
- assert received_value == send_property_value, (
707
- f"Received value for {send_property_name} '{received_value}' does not "
708
- f"match '{send_property_value}' in the {response.request.method} request."
709
- f"\nSend: {_json.dumps(send_json, indent=4, sort_keys=True)}"
710
- f"\nGot: {_json.dumps(response_data, indent=4, sort_keys=True)}"
711
- )
712
-
713
- if response.request.body is None:
714
- logger.warning(
715
- "Could not validate send response; the body of the request property "
716
- "on the provided response was None."
717
- )
718
- return None
719
- if isinstance(response.request.body, bytes):
720
- send_json = _json.loads(response.request.body.decode("UTF-8"))
721
- else:
722
- send_json = _json.loads(response.request.body)
723
-
724
- response_data = response.json()
725
- # POST on /resource_type/{id}/array_item/ will return the updated {id} resource
726
- # instead of a newly created resource. In this case, the send_json must be
727
- # in the array of the 'array_item' property on {id}
728
- send_path: str = response.request.path_url
729
- response_path = response_data.get("href", None)
730
- if response_path and send_path not in response_path:
731
- property_to_check = send_path.replace(response_path, "")[1:]
732
- if response_data.get(property_to_check) and isinstance(
733
- response_data[property_to_check], list
734
- ):
735
- item_list: List[Dict[str, Any]] = response_data[property_to_check]
736
- # Use the (mandatory) id to get the POSTed resource from the list
737
- [response_data] = [
738
- item for item in item_list if item["id"] == send_json["id"]
739
- ]
740
-
741
- # incoming arguments are dictionaries, so they can be validated as such
742
- validate_dict_response(send_dict=send_json, received_dict=response_data)
743
-
744
- # In case of PATCH requests, ensure that only send properties have changed
745
- if original_data:
746
- for send_property_name, send_value in original_data.items():
747
- if send_property_name not in send_json.keys():
748
- assert send_value == response_data[send_property_name], (
749
- f"Received value for {send_property_name} '{response_data[send_property_name]}' does not "
750
- f"match '{send_value}' in the pre-patch data"
751
- f"\nPre-patch: {_json.dumps(original_data, indent=4, sort_keys=True)}"
752
- f"\nGot: {_json.dumps(response_data, indent=4, sort_keys=True)}"
753
- )
754
- return None
755
-
756
- def _get_response_spec(
757
- self, path: str, method: str, status_code: int
758
- ) -> Dict[str, Any]:
759
- method = method.lower()
760
- status = str(status_code)
761
- spec: Dict[str, Any] = {**self.openapi_spec}["paths"][path][method][
762
- "responses"
763
- ][status]
764
- return spec
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2024-04-12T13:08:14+00:00" specversion="5" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapidriver.py" lineno="352">
3
- <version>0.2.0</version>
2
+ <keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2024-05-15T18:47:16+00:00" specversion="5" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapidriver.py" lineno="352">
3
+ <version>0.2.2</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>
@@ -199,7 +199,7 @@
199
199
  </init>
200
200
  </inits>
201
201
  <keywords>
202
- <kw name="Test Endpoint" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="175">
202
+ <kw name="Test Endpoint" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="156">
203
203
  <arguments repr="path: str, method: str, status_code: int">
204
204
  <arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
205
205
  <name>path</name>
@@ -219,7 +219,7 @@
219
219
  &lt;p&gt;The keyword calls other keywords to generate the neccesary data to perform the desired operation and validate the response against the openapi document.&lt;/p&gt;</doc>
220
220
  <shortdoc>Validate that performing the `method` operation on `path` results in a `status_code` response.</shortdoc>
221
221
  </kw>
222
- <kw name="Test Forbidden" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="116">
222
+ <kw name="Test Forbidden" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="97">
223
223
  <arguments repr="path: str, method: str">
224
224
  <arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
225
225
  <name>path</name>
@@ -235,7 +235,7 @@
235
235
  &lt;p&gt;For this keyword to pass, the authorization parameters used to initialize the library should grant insufficient access rights to the target endpoint. &amp;gt; Note: No headers or (json) body are send with the request. For security reasons, the access rights validation should be checked first.&lt;/p&gt;</doc>
236
236
  <shortdoc>Perform a request for `method` on the `path`, with the provided authorization.</shortdoc>
237
237
  </kw>
238
- <kw name="Test Invalid Url" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="133">
238
+ <kw name="Test Invalid Url" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="114">
239
239
  <arguments repr="path: str, method: str, expected_status_code: int = 404">
240
240
  <arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
241
241
  <name>path</name>
@@ -257,7 +257,7 @@
257
257
  &lt;p&gt;&amp;gt; Note: Depending on API design, the url may be validated before or after validation of headers, query parameters and / or (json) body. By default, no parameters are send with the request. The &lt;span class="name"&gt;require_body_for_invalid_url&lt;/span&gt; parameter can be set to &lt;span class="name"&gt;True&lt;/span&gt; if needed.&lt;/p&gt;</doc>
258
258
  <shortdoc>Perform a request for the provided 'path' and 'method' where the url for the `path` is invalidated.</shortdoc>
259
259
  </kw>
260
- <kw name="Test Unauthorized" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="95">
260
+ <kw name="Test Unauthorized" source="/workspaces/robotframework-openapitools/src/OpenApiDriver/openapi_executors.py" lineno="76">
261
261
  <arguments repr="path: str, method: str">
262
262
  <arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
263
263
  <name>path</name>
@@ -1,48 +1,54 @@
1
- """
2
- The OpenApiLibCore package is intended to be used as a dependency for other
3
- Robot Framework libraries that facilitate the testing of OpenAPI / Swagger APIs.
4
- The following classes and constants are exposed to be used by the library user:
5
- - OpenApiLibCore: The class to be imported in the Robot Framework library.
6
- - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
7
- UniquePropertyValueConstraint: Classes to be subclassed by the library user
8
- when implementing a custom mapping module (advanced use).
9
- - Dto, Relation: Base classes that can be used for type annotations.
10
- - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
11
- """
12
-
13
- from importlib.metadata import version
14
-
15
- from OpenApiLibCore.dto_base import (
16
- Dto,
17
- IdDependency,
18
- IdReference,
19
- PathPropertiesConstraint,
20
- PropertyValueConstraint,
21
- Relation,
22
- UniquePropertyValueConstraint,
23
- resolve_schema,
24
- )
25
- from OpenApiLibCore.dto_utils import DefaultDto
26
- from OpenApiLibCore.openapi_libcore import OpenApiLibCore, RequestData, RequestValues
27
- from OpenApiLibCore.value_utils import IGNORE
28
-
29
- try:
30
- __version__ = version("robotframework-openapi-libcore")
31
- except Exception: # pragma: no cover
32
- pass
33
-
34
- __all__ = [
35
- "Dto",
36
- "IdDependency",
37
- "IdReference",
38
- "PathPropertiesConstraint",
39
- "PropertyValueConstraint",
40
- "Relation",
41
- "UniquePropertyValueConstraint",
42
- "DefaultDto",
43
- "OpenApiLibCore",
44
- "RequestData",
45
- "RequestValues",
46
- "resolve_schema",
47
- "IGNORE",
48
- ]
1
+ """
2
+ The OpenApiLibCore package is intended to be used as a dependency for other
3
+ Robot Framework libraries that facilitate the testing of OpenAPI / Swagger APIs.
4
+ The following classes and constants are exposed to be used by the library user:
5
+ - OpenApiLibCore: The class to be imported in the Robot Framework library.
6
+ - IdDependency, IdReference, PathPropertiesConstraint, PropertyValueConstraint,
7
+ UniquePropertyValueConstraint: Classes to be subclassed by the library user
8
+ when implementing a custom mapping module (advanced use).
9
+ - Dto, Relation: Base classes that can be used for type annotations.
10
+ - IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
11
+ """
12
+
13
+ from importlib.metadata import version
14
+
15
+ from OpenApiLibCore.dto_base import (
16
+ Dto,
17
+ IdDependency,
18
+ IdReference,
19
+ PathPropertiesConstraint,
20
+ PropertyValueConstraint,
21
+ Relation,
22
+ UniquePropertyValueConstraint,
23
+ resolve_schema,
24
+ )
25
+ from OpenApiLibCore.dto_utils import DefaultDto
26
+ from OpenApiLibCore.openapi_libcore import (
27
+ OpenApiLibCore,
28
+ RequestData,
29
+ RequestValues,
30
+ ValidationLevel,
31
+ )
32
+ from OpenApiLibCore.value_utils import IGNORE
33
+
34
+ try:
35
+ __version__ = version("robotframework-openapi-libcore")
36
+ except Exception: # pragma: no cover
37
+ pass
38
+
39
+ __all__ = [
40
+ "Dto",
41
+ "IdDependency",
42
+ "IdReference",
43
+ "PathPropertiesConstraint",
44
+ "PropertyValueConstraint",
45
+ "Relation",
46
+ "UniquePropertyValueConstraint",
47
+ "DefaultDto",
48
+ "OpenApiLibCore",
49
+ "RequestData",
50
+ "RequestValues",
51
+ "ValidationLevel",
52
+ "resolve_schema",
53
+ "IGNORE",
54
+ ]
@@ -76,10 +76,10 @@ def merge_schemas(first: Dict[str, Any], second: Dict[str, Any]) -> Dict[str, An
76
76
  elif isinstance(value, list):
77
77
  # if the key holds a list, extend the values (e.g. 'required')
78
78
  merged_schema[key].extend(value)
79
- else:
79
+ elif value != merged_schema[key]:
80
80
  logger.debug(
81
- f"key '{key}' with value '{merged_schema[key]}' not "
82
- f"updated to '{value}'"
81
+ f"key '{key}' with value '{merged_schema[key]}'"
82
+ f" not updated to '{value}'"
83
83
  )
84
84
  else:
85
85
  merged_schema[key] = value