Skip to content
This repository has been archived by the owner on Aug 9, 2024. It is now read-only.

Support python 312 #39

Merged
merged 8 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM python:3.12-slim-bookworm

RUN pip install --upgrade pip

# poetry install into the default Python interpreter since we're in a container
RUN pip install poetry
RUN poetry config virtualenvs.create false
RUN poetry config virtualenvs.in-project false

# Copy the pyproject.toml and poetry.lock file to be able to install dependencies using poetry
COPY pyproject.toml pyproject.toml
COPY poetry.lock poetry.lock

EXPOSE 8888
ENTRYPOINT /bin/sh
34 changes: 34 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "Local Dockerfile",
"build": {
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerfile": "./Dockerfile",
"args": {
}
},
"postCreateCommand": "poetry install",
// Configure tool-specific properties.
"customizations": {
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {
"robotcode.robot.variables": {
"ROOT": "/workspaces/robotframework-openapidriver"
}
},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"d-biehl.robotcode",
"ms-azuretools.vscode-docker",
"Gruntfuggly.todo-tree",
"shardulm94.trailing-spaces"
]
}
}
}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ report.html
tests/logs

# IDE config
.vscode
.vscode/launch.json
.vscode/settings.json

# PowerShell utility scripts
_*.ps1
33 changes: 33 additions & 0 deletions .vscode/example.launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "RobotCode: Default",
"type": "robotcode",
"request": "launch",
"purpose": "default",
"presentation": {
"hidden": true
},
"attachPython": true,
"pythonConfiguration": "RobotCode: Python",
"args": [
"--variable=ROOT:${workspaceFolder}",
"--outputdir=${workspaceFolder}/tests/logs",
"--loglevel=TRACE:INFO"
]
},
{
"name": "RobotCode: Python",
"type": "python",
"request": "attach",
"presentation": {
"hidden": true
},
"justMyCode": false
}
]
}
5 changes: 5 additions & 0 deletions .vscode/example.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"robotcode.robot.variables": {
"ROOT": "/workspaces/robotframework-openapidriver"
}
}
9 changes: 9 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"recommendations": [
"ms-vscode-remote.remote-containers",
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"d-biehl.robotcode"
]
}
4 changes: 2 additions & 2 deletions docs/openapidriver.html

Large diffs are not rendered by default.

1,671 changes: 857 additions & 814 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name="robotframework-openapidriver"
version = "4.2.1"
version = "4.3.0"
description = "A library for contract-testing OpenAPI / Swagger APIs."
license = "Apache-2.0"
authors = ["Robin Mackaij <r.a.mackaij@gmail.com>"]
Expand All @@ -24,7 +24,7 @@ include = ["*.libspec"]
[tool.poetry.dependencies]
python = "^3.8"
robotframework-datadriver = ">=1.6.1"
robotframework-openapi-libcore = "^1.10.1"
robotframework-openapi-libcore = "^1.11.0"

[tool.poetry.group.dev.dependencies]
fastapi = ">=0.95.0"
Expand Down Expand Up @@ -96,9 +96,11 @@ py_version=38

[tool.ruff]
line-length = 120
select = ["E", "F", "PL"]
src = ["src/OpenApiDriver"]

[tool.ruff.lint]
select = ["E", "F", "PL"]

[tool.pylint.'MESSAGES CONTROL']
disable = ["logging-fstring-interpolation", "missing-class-docstring"]

Expand Down
1 change: 1 addition & 0 deletions src/OpenApiDriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Dto, Relation: Base classes that can be used for type annotations.
- IGNORE: A special constant that can be used as a value in the PropertyValueConstraint.
"""

from importlib.metadata import version

from OpenApiLibCore.dto_base import (
Expand Down
72 changes: 40 additions & 32 deletions src/OpenApiDriver/openapi_executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
RequestsOpenAPIRequest,
RequestsOpenAPIResponse,
)
from openapi_core.templating.paths.exceptions import ServerNotFound
from openapi_core.exceptions import OpenAPIError
from openapi_core.validation.exceptions import ValidationError
from openapi_core.validation.response.exceptions import InvalidData
from openapi_core.validation.schemas.exceptions import InvalidSchemaValue
from OpenApiLibCore import OpenApiLibCore, RequestData, RequestValues, resolve_schema
from requests import Response
from requests.auth import AuthBase
from requests.cookies import RequestsCookieJar as CookieJar
from robot.api import SkipExecution
from robot.api import Failure, SkipExecution
from robot.api.deco import keyword, library
from robot.libraries.BuiltIn import BuiltIn

Expand Down Expand Up @@ -338,6 +341,7 @@ def perform_validated_request(
)

run_keyword("validate_response", path, response, original_data)

if request_values.method == "DELETE":
get_request_data = self.get_request_data(endpoint=path, method="GET")
get_params = get_request_data.params
Expand Down Expand Up @@ -383,8 +387,11 @@ def validate_response(
if response.status_code == 204:
assert not response.content
return None
# validate the response against the schema
self._validate_response_against_spec(response)

try:
self._validate_response_against_spec(response)
except OpenAPIError:
raise Failure("Response did not pass schema validation.")

request_method = response.request.method
if request_method is None:
Expand Down Expand Up @@ -475,35 +482,36 @@ def _assert_href_is_valid(self, href: str, json_response: Dict[str, Any]) -> Non
), f"{get_response.json()} not equal to original {json_response}"

def _validate_response_against_spec(self, response: Response) -> None:
validation_result = self.validate_response_vs_spec(
request=RequestsOpenAPIRequest(response.request),
response=RequestsOpenAPIResponse(response),
)
if self.disable_server_validation:
validation_result.errors = [
e for e in validation_result.errors if not isinstance(e, ServerNotFound)
]

# The OAS concepts of optional / nullable are not compatible with Python.
# Filter the errors caused by this incompatibility.
errors_to_keep = []
for error in validation_result.errors:
message = str(error)
if message == "None for not nullable" or message.startswith("None is not "):
logger.debug("'None for not nullable' OpenAPIError ignored.")
try:
self.validate_response_vs_spec(
request=RequestsOpenAPIRequest(response.request),
response=RequestsOpenAPIResponse(response),
)
except InvalidData as exception:
errors: List[InvalidSchemaValue] = exception.__cause__
validation_errors: Optional[List[ValidationError]] = getattr(
errors, "schema_errors", None
)
if validation_errors:
error_message = "\n".join(
[
f"{list(error.schema_path)}: {error.message}"
for error in validation_errors
]
)
else:
errors_to_keep.append(error)

validation_result.errors = errors_to_keep

if self.response_validation == ValidationLevel.STRICT:
validation_result.raise_for_errors()
if self.response_validation in [ValidationLevel.WARN, ValidationLevel.INFO]:
for validation_error in validation_result.errors:
if self.response_validation == ValidationLevel.WARN:
logger.warning(validation_error)
else:
logger.info(validation_error)
error_message = str(exception)

if response.status_code == self.invalid_property_default_response:
logger.debug(error_message)
return
if self.response_validation == ValidationLevel.STRICT:
logger.error(error_message)
raise exception
if self.response_validation == ValidationLevel.WARN:
logger.warning(error_message)
elif self.response_validation == ValidationLevel.INFO:
logger.info(error_message)

@keyword
def validate_resource_properties(
Expand Down
1 change: 1 addition & 0 deletions src/OpenApiDriver/openapi_reader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Module holding the OpenApiReader reader_class implementation."""

from typing import Any, Dict, List, Union

from DataDriver.AbstractReaderClass import AbstractReaderClass
Expand Down
12 changes: 6 additions & 6 deletions src/OpenApiDriver/openapidriver.libspec
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2023-07-28T14:31:11+00:00" specversion="5" source="C:\GitHub\robotframework-openapidriver\src\OpenApiDriver\openapidriver.py" lineno="351">
<version>4.2.0</version>
<keywordspec name="OpenApiDriver" type="LIBRARY" format="HTML" scope="SUITE" generated="2024-02-12T13:49:58+00:00" specversion="5" source="/workspaces/robotframework-openapidriver/src/OpenApiDriver/openapidriver.py" lineno="352">
<version>4.3.0</version>
<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>
<tags>
</tags>
<inits>
<init name="__init__" lineno="143">
<init name="__init__" lineno="144">
<arguments repr="source: str, origin: str = , base_path: str = , included_paths: Iterable[str] | None = None, ignored_paths: Iterable[str] | None = None, ignored_responses: Iterable[int] | None = None, ignored_testcases: Iterable[Tuple[str, str, int]] | None = None, response_validation: ValidationLevel = WARN, disable_server_validation: bool = True, mappings_path: str | Path = , invalid_property_default_response: int = 422, default_id_property_name: str = id, faker_locale: str | List[str] | None = None, require_body_for_invalid_url: bool = False, recursion_limit: int = 1, recursion_default: Any = {}, username: str = , password: str = , security_token: str = , auth: AuthBase | None = None, cert: str | Tuple[str, str] | None = None, verify_tls: bool | str | None = True, extra_headers: Dict[str, str] | None = None, cookies: Dict[str, str] | RequestsCookieJar | None = None, proxies: Dict[str, str] | None = None">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="source: str">
<name>source</name>
Expand Down Expand Up @@ -199,7 +199,7 @@
</init>
</inits>
<keywords>
<kw name="Test Endpoint" source="C:\GitHub\robotframework-openapidriver\src\OpenApiDriver\openapi_executors.py" lineno="154">
<kw name="Test Endpoint" source="/workspaces/robotframework-openapidriver/src/OpenApiDriver/openapi_executors.py" lineno="156">
<arguments repr="path: str, method: str, status_code: int">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
<name>path</name>
Expand All @@ -219,7 +219,7 @@
&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>
<shortdoc>Validate that performing the `method` operation on `path` results in a `status_code` response.</shortdoc>
</kw>
<kw name="Test Invalid Url" source="C:\GitHub\robotframework-openapidriver\src\OpenApiDriver\openapi_executors.py" lineno="112">
<kw name="Test Invalid Url" source="/workspaces/robotframework-openapidriver/src/OpenApiDriver/openapi_executors.py" lineno="114">
<arguments repr="path: str, method: str, expected_status_code: int = 404">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
<name>path</name>
Expand All @@ -241,7 +241,7 @@
&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>
<shortdoc>Perform a request for the provided 'path' and 'method' where the url for the `path` is invalidated.</shortdoc>
</kw>
<kw name="Test Unauthorized" source="C:\GitHub\robotframework-openapidriver\src\OpenApiDriver\openapi_executors.py" lineno="92">
<kw name="Test Unauthorized" source="/workspaces/robotframework-openapidriver/src/OpenApiDriver/openapi_executors.py" lineno="94">
<arguments repr="path: str, method: str">
<arg kind="POSITIONAL_OR_NAMED" required="true" repr="path: str">
<name>path</name>
Expand Down
1 change: 1 addition & 0 deletions src/OpenApiDriver/openapidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
- No support for per-path authorization levels (only simple 401 / 403 validation).

"""

from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union

Expand Down
3 changes: 2 additions & 1 deletion tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import subprocess
from importlib.metadata import version

from invoke import task, Context
from invoke.context import Context
from invoke.tasks import task

from OpenApiDriver import openapidriver

Expand Down
4 changes: 3 additions & 1 deletion tests/server/testserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ def patch_employee(employee_id: str, employee: EmployeeUpdate) -> EmployeeDetail
if employee_id not in EMPLOYEES.keys():
raise HTTPException(status_code=404, detail="Employee not found")
stored_employee_data = EMPLOYEES[employee_id]
employee_update_data = employee.model_dump(exclude_unset=True)
employee_update_data = employee.model_dump(
exclude_defaults=True, exclude_unset=True
)

wagegroup_id = employee_update_data.get("wagegroup_id", None)
if wagegroup_id and wagegroup_id not in WAGE_GROUPS:
Expand Down
2 changes: 1 addition & 1 deletion tests/suites/load_from_url.robot
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Library OpenApiDriver
... origin=http://localhost:8000
... base_path=${EMPTY}
... mappings_path=${ROOT}/tests/user_implemented/custom_user_mappings.py
... response_validation=INFO
... response_validation=STRICT
... require_body_for_invalid_url=${TRUE}
... extra_headers=${API_KEY}
... faker_locale=nl_NL
Expand Down
Loading