"]
license = "BSD-3-Clause"
-readme = "README.rst"
+readme = "README.md"
repository = "https://github.com/python-openapi/openapi-core"
documentation = "https://openapi-core.readthedocs.io"
keywords = ["openapi", "swagger", "schema"]
@@ -47,7 +47,6 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -60,24 +59,27 @@ include = [
]
[tool.poetry.dependencies]
-python = "^3.8.0"
+python = "^3.9.0"
django = {version = ">=3.0", optional = true}
falcon = {version = ">=3.0", optional = true}
flask = {version = "*", optional = true}
aiohttp = {version = ">=3.0", optional = true}
-starlette = {version = ">=0.26.1,<0.38.0", optional = true}
+starlette = {version = ">=0.26.1,<0.47.0", optional = true}
isodate = "*"
more-itertools = "*"
parse = "*"
openapi-schema-validator = "^0.6.0"
openapi-spec-validator = "^0.7.1"
requests = {version = "*", optional = true}
-werkzeug = "*"
-jsonschema-path = "^0.3.1"
-jsonschema = "^4.18.0"
+# werkzeug 3.1.2 changed the definition of Headers
+# See https://github.com/python-openapi/openapi-core/issues/938
+werkzeug = "<3.1.2"
+jsonschema-path = "^0.3.4"
+jsonschema = "^4.23.0"
multidict = {version = "^6.0.4", optional = true}
-aioitertools = {version = "^0.11.0", optional = true}
-fastapi = {version = "^0.108.0", optional = true}
+aioitertools = {version = ">=0.11,<0.13", optional = true}
+fastapi = {version = ">=0.111,<0.116", optional = true}
+typing-extensions = "^4.8.0"
[tool.poetry.extras]
django = ["django"]
@@ -101,21 +103,23 @@ pytest-flake8 = "*"
pytest-cov = "*"
python-multipart = "*"
responses = "*"
-starlette = ">=0.26.1,<0.38.0"
+starlette = ">=0.26.1,<0.47.0"
strict-rfc3339 = "^0.7"
webob = "*"
mypy = "^1.2"
-httpx = ">=0.24,<0.27"
-deptry = ">=0.11,<0.17"
+httpx = ">=0.24,<0.29"
+deptry = ">=0.11,<0.21"
aiohttp = "^3.8.4"
pytest-aiohttp = "^1.0.4"
bump2version = "^1.0.1"
pyflakes = "^3.1.0"
-fastapi = "^0.108.0"
+fastapi = ">=0.111,<0.116"
[tool.poetry.group.docs.dependencies]
-sphinx = ">=5.3,<8.0"
-sphinx-immaterial = "^0.11.0"
+mkdocs = "^1.6.1"
+mkdocstrings = {extras = ["python"], version = "^0.26.1"}
+mkdocs-material = "^9.5.34"
+griffe-typingdoc = "^0.2.7"
[tool.pytest.ini_options]
addopts = """
diff --git a/tests/integration/contrib/aiohttp/test_aiohttp_project.py b/tests/integration/contrib/aiohttp/test_aiohttp_project.py
index 9b67705a..54f7297d 100644
--- a/tests/integration/contrib/aiohttp/test_aiohttp_project.py
+++ b/tests/integration/contrib/aiohttp/test_aiohttp_project.py
@@ -1,6 +1,7 @@
import os
import sys
from base64 import b64encode
+from io import BytesIO
import pytest
@@ -63,7 +64,7 @@ async def test_post_valid(self, client, data_gif):
"Host": "petstore.swagger.io",
}
data = {
- "file": data_gif,
+ "file": BytesIO(data_gif),
}
cookies = {"user": "1"}
diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/__init__.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/migrations/__init__.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py
new file mode 100644
index 00000000..10d87749
--- /dev/null
+++ b/tests/integration/contrib/django/data/v3.0/djangoproject/status/views.py
@@ -0,0 +1,17 @@
+from pathlib import Path
+
+from django.http import HttpResponse
+from jsonschema_path import SchemaPath
+
+from openapi_core.contrib.django.decorators import DjangoOpenAPIViewDecorator
+
+check_minimal_spec = DjangoOpenAPIViewDecorator.from_spec(
+ SchemaPath.from_file_path(
+ Path("tests/integration/data/v3.0/minimal_with_servers.yaml")
+ )
+)
+
+
+@check_minimal_spec
+def get_status(request):
+ return HttpResponse("OK")
diff --git a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py
index ff987972..be4e9781 100644
--- a/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py
+++ b/tests/integration/contrib/django/data/v3.0/djangoproject/urls.py
@@ -20,6 +20,7 @@
from djangoproject.pets.views import PetDetailView
from djangoproject.pets.views import PetListView
from djangoproject.pets.views import PetPhotoView
+from djangoproject.status.views import get_status
from djangoproject.tags.views import TagListView
urlpatterns = [
@@ -48,4 +49,9 @@
TagListView.as_view(),
name="tag_list_view",
),
+ path(
+ "status",
+ get_status,
+ name="get_status_view",
+ ),
]
diff --git a/tests/integration/contrib/django/test_django_project.py b/tests/integration/contrib/django/test_django_project.py
index 6614eeaf..8a0697e1 100644
--- a/tests/integration/contrib/django/test_django_project.py
+++ b/tests/integration/contrib/django/test_django_project.py
@@ -184,7 +184,7 @@ def test_post_media_type_invalid(self, client):
"title": (
"Content for the following mimetype not found: "
"text/html. "
- "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
+ "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
),
}
]
@@ -422,3 +422,41 @@ def test_post_valid(self, client, data_gif):
assert response.status_code == 201
assert not response.content
+
+
+class TestStatusView(BaseTestDjangoProject):
+
+ def test_get_valid(self, client, data_gif):
+ headers = {
+ "HTTP_AUTHORIZATION": "Basic testuser",
+ "HTTP_HOST": "petstore.swagger.io",
+ }
+ from django.conf import settings
+
+ MIDDLEWARE = [
+ v for v in settings.MIDDLEWARE if "openapi_core" not in v
+ ]
+ with override_settings(MIDDLEWARE=MIDDLEWARE):
+ response = client.get("/status", **headers)
+
+ assert response.status_code == 200
+ assert response.content.decode() == "OK"
+
+ def test_post_valid(self, client):
+ data = {"key": "value"}
+ content_type = "application/json"
+ headers = {
+ "HTTP_AUTHORIZATION": "Basic testuser",
+ "HTTP_HOST": "petstore.swagger.io",
+ }
+ from django.conf import settings
+
+ MIDDLEWARE = [
+ v for v in settings.MIDDLEWARE if "openapi_core" not in v
+ ]
+ with override_settings(MIDDLEWARE=MIDDLEWARE):
+ response = client.post(
+ "/status", data=data, content_type=content_type, **headers
+ )
+
+ assert response.status_code == 405 # Method Not Allowed
diff --git a/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py b/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py
index 5d0a83f4..d6e903da 100644
--- a/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py
+++ b/tests/integration/contrib/falcon/data/v3.0/falconproject/pets/resources.py
@@ -11,11 +11,19 @@ class PetListResource:
def on_get(self, request, response):
assert request.context.openapi
assert not request.context.openapi.errors
- assert request.context.openapi.parameters.query == {
- "page": 1,
- "limit": 12,
- "search": "",
- }
+ if "ids" in request.params:
+ assert request.context.openapi.parameters.query == {
+ "page": 1,
+ "limit": 2,
+ "search": "",
+ "ids": [1, 2],
+ }
+ else:
+ assert request.context.openapi.parameters.query == {
+ "page": 1,
+ "limit": 12,
+ "search": "",
+ }
data = [
{
"id": 12,
diff --git a/tests/integration/contrib/falcon/test_falcon_project.py b/tests/integration/contrib/falcon/test_falcon_project.py
index 7ed3a19c..252e0d6a 100644
--- a/tests/integration/contrib/falcon/test_falcon_project.py
+++ b/tests/integration/contrib/falcon/test_falcon_project.py
@@ -2,6 +2,7 @@
from json import dumps
import pytest
+from urllib3 import encode_multipart_formdata
class BaseTestFalconProject:
@@ -67,6 +68,33 @@ def test_get_valid(self, client):
],
}
+ def test_get_valid_multiple_ids(self, client):
+ headers = {
+ "Content-Type": "application/json",
+ }
+ query_string = "limit=2&ids=1&ids=2"
+
+ with pytest.warns(DeprecationWarning):
+ response = client.simulate_get(
+ "/v1/pets",
+ host="petstore.swagger.io",
+ headers=headers,
+ query_string=query_string,
+ )
+
+ assert response.status_code == 200
+ assert response.json == {
+ "data": [
+ {
+ "id": 12,
+ "name": "Cat",
+ "ears": {
+ "healthy": True,
+ },
+ },
+ ],
+ }
+
def test_post_server_invalid(self, client):
response = client.simulate_post(
"/v1/pets",
@@ -177,7 +205,7 @@ def test_post_media_type_invalid(self, client):
"title": (
"Content for the following mimetype not found: "
f"{content_type}. "
- "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
+ "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
),
}
]
@@ -265,6 +293,43 @@ def test_post_valid(self, client, data_json):
assert response.status_code == 201
assert not response.content
+ @pytest.mark.xfail(
+ reason="falcon multipart form serialization unsupported",
+ strict=True,
+ )
+ def test_post_multipart_valid(self, client, data_gif):
+ cookies = {"user": 1}
+ auth = "authuser"
+ fields = {
+ "name": "Cat",
+ "address": (
+ "aaddress.json",
+ dumps(dict(city="Warsaw")),
+ "application/json",
+ ),
+ "photo": (
+ "photo.jpg",
+ data_gif,
+ "image/jpeg",
+ ),
+ }
+ body, content_type_header = encode_multipart_formdata(fields)
+ headers = {
+ "Authorization": f"Basic {auth}",
+ "Content-Type": content_type_header,
+ }
+
+ response = client.simulate_post(
+ "/v1/pets",
+ host="staging.gigantic-server.com",
+ headers=headers,
+ body=body,
+ cookies=cookies,
+ protocol="https",
+ )
+
+ assert response.status_code == 200
+
class TestPetDetailResource:
def test_get_server_invalid(self, client):
diff --git a/tests/integration/contrib/fastapi/test_fastapi_project.py b/tests/integration/contrib/fastapi/test_fastapi_project.py
index e8d795c6..242613bc 100644
--- a/tests/integration/contrib/fastapi/test_fastapi_project.py
+++ b/tests/integration/contrib/fastapi/test_fastapi_project.py
@@ -183,7 +183,7 @@ def test_post_media_type_invalid(self, client):
"title": (
"Content for the following mimetype not found: "
"text/html. "
- "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
+ "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
),
}
]
diff --git a/tests/integration/contrib/starlette/test_starlette_project.py b/tests/integration/contrib/starlette/test_starlette_project.py
index fc799a30..d1e8ed54 100644
--- a/tests/integration/contrib/starlette/test_starlette_project.py
+++ b/tests/integration/contrib/starlette/test_starlette_project.py
@@ -183,7 +183,7 @@ def test_post_media_type_invalid(self, client):
"title": (
"Content for the following mimetype not found: "
"text/html. "
- "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']"
+ "Valid mimetypes: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain']"
),
}
]
diff --git a/tests/integration/data/v3.0/parent-reference/openapi.yaml b/tests/integration/data/v3.0/parent-reference/openapi.yaml
new file mode 100644
index 00000000..51150416
--- /dev/null
+++ b/tests/integration/data/v3.0/parent-reference/openapi.yaml
@@ -0,0 +1,7 @@
+openapi: "3.0.0"
+info:
+ title: sample
+ version: "0.1"
+paths:
+ /books:
+ $ref: "./paths/books.yaml"
\ No newline at end of file
diff --git a/tests/integration/data/v3.0/parent-reference/paths/books.yaml b/tests/integration/data/v3.0/parent-reference/paths/books.yaml
new file mode 100644
index 00000000..d625f4f5
--- /dev/null
+++ b/tests/integration/data/v3.0/parent-reference/paths/books.yaml
@@ -0,0 +1,10 @@
+get:
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "../schemas/book.yaml#/Book"
\ No newline at end of file
diff --git a/tests/integration/data/v3.0/parent-reference/schemas/book.yaml b/tests/integration/data/v3.0/parent-reference/schemas/book.yaml
new file mode 100644
index 00000000..1bf35402
--- /dev/null
+++ b/tests/integration/data/v3.0/parent-reference/schemas/book.yaml
@@ -0,0 +1,9 @@
+Book:
+ type: object
+ properties:
+ id:
+ $ref: "#/BookId"
+ title:
+ type: string
+BookId:
+ type: string
\ No newline at end of file
diff --git a/tests/integration/data/v3.0/petstore.yaml b/tests/integration/data/v3.0/petstore.yaml
index d26816ac..735fd96c 100644
--- a/tests/integration/data/v3.0/petstore.yaml
+++ b/tests/integration/data/v3.0/petstore.yaml
@@ -150,6 +150,9 @@ paths:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/PetCreate'
+ multipart/form-data:
+ schema:
+ $ref: '#/components/schemas/PetWithPhotoCreate'
text/plain: {}
responses:
'201':
@@ -375,6 +378,16 @@ components:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Bird"
+ PetWithPhotoCreate:
+ type: object
+ x-model: PetWithPhotoCreate
+ allOf:
+ - $ref: "#/components/schemas/PetCreatePartOne"
+ - $ref: "#/components/schemas/PetCreatePartTwo"
+ - $ref: "#/components/schemas/PetCreatePartPhoto"
+ oneOf:
+ - $ref: "#/components/schemas/Cat"
+ - $ref: "#/components/schemas/Bird"
PetCreatePartOne:
type: object
x-model: PetCreatePartOne
@@ -395,6 +408,15 @@ components:
$ref: "#/components/schemas/Position"
healthy:
type: boolean
+ PetCreatePartPhoto:
+ type: object
+ x-model: PetCreatePartPhoto
+ properties:
+ photo:
+ $ref: "#/components/schemas/PetPhoto"
+ PetPhoto:
+ type: string
+ format: binary
Bird:
type: object
x-model: Bird
diff --git a/tests/integration/unmarshalling/test_request_unmarshaller.py b/tests/integration/unmarshalling/test_request_unmarshaller.py
index 2993275b..0eefa3f0 100644
--- a/tests/integration/unmarshalling/test_request_unmarshaller.py
+++ b/tests/integration/unmarshalling/test_request_unmarshaller.py
@@ -201,6 +201,7 @@ def test_invalid_content_type(self, request_unmarshaller):
availableMimetypes=[
"application/json",
"application/x-www-form-urlencoded",
+ "multipart/form-data",
"text/plain",
],
)
diff --git a/tests/integration/unmarshalling/test_unmarshallers.py b/tests/integration/unmarshalling/test_unmarshallers.py
index 764fc3af..54e944a3 100644
--- a/tests/integration/unmarshalling/test_unmarshallers.py
+++ b/tests/integration/unmarshalling/test_unmarshallers.py
@@ -1840,6 +1840,25 @@ def test_object_property_nullable(self, unmarshallers_factory):
assert result == value
+ def test_subschema_nullable(self, unmarshallers_factory):
+ schema = {
+ "oneOf": [
+ {
+ "type": "integer",
+ },
+ {
+ "nullable": True,
+ },
+ ]
+ }
+ spec = SchemaPath.from_dict(schema)
+ unmarshaller = unmarshallers_factory.create(spec)
+ value = None
+
+ result = unmarshaller.unmarshal(value)
+
+ assert result is None
+
class TestOAS30RequestSchemaUnmarshallersFactory(
BaseTestOASSchemaUnmarshallersFactoryCall,
@@ -2086,3 +2105,22 @@ def test_any_null(self, unmarshallers_factory):
result = unmarshaller.unmarshal(None)
assert result is None
+
+ def test_subschema_null(self, unmarshallers_factory):
+ schema = {
+ "oneOf": [
+ {
+ "type": "integer",
+ },
+ {
+ "type": "null",
+ },
+ ]
+ }
+ spec = SchemaPath.from_dict(schema)
+ unmarshaller = unmarshallers_factory.create(spec)
+ value = None
+
+ result = unmarshaller.unmarshal(value)
+
+ assert result is None
diff --git a/tests/integration/validation/test_parent_reference.py b/tests/integration/validation/test_parent_reference.py
new file mode 100644
index 00000000..21e37351
--- /dev/null
+++ b/tests/integration/validation/test_parent_reference.py
@@ -0,0 +1,45 @@
+import json
+
+import pytest
+from jsonschema_path import SchemaPath
+
+from openapi_core import Config
+from openapi_core import OpenAPI
+from openapi_core import V30ResponseUnmarshaller
+from openapi_core.testing import MockRequest
+from openapi_core.testing import MockResponse
+
+
+class TestParentReference:
+
+ spec_path = "data/v3.0/parent-reference/openapi.yaml"
+
+ @pytest.fixture
+ def unmarshaller(self, content_factory):
+ content, base_uri = content_factory.from_file(self.spec_path)
+ return V30ResponseUnmarshaller(
+ spec=SchemaPath.from_dict(content, base_uri=base_uri)
+ )
+
+ @pytest.fixture
+ def openapi(self, content_factory):
+ content, base_uri = content_factory.from_file(self.spec_path)
+ spec = SchemaPath.from_dict(content, base_uri=base_uri)
+ config = Config(spec_base_uri=base_uri)
+ return OpenAPI(spec, config=config)
+
+ def test_valid(self, openapi):
+ request = MockRequest(host_url="", method="GET", path="/books")
+ response = MockResponse(
+ data=json.dumps([{"id": "BOOK:01", "title": "Test Book"}]).encode()
+ )
+
+ openapi.validate_response(request, response)
+
+ def test_unmarshal(self, unmarshaller):
+ request = MockRequest(host_url="", method="GET", path="/books")
+ response = MockResponse(
+ data=json.dumps([{"id": "BOOK:01", "title": "Test Book"}]).encode()
+ )
+
+ unmarshaller.unmarshal(request, response)
diff --git a/tests/integration/validation/test_request_validators.py b/tests/integration/validation/test_request_validators.py
index 14a7e6d6..eaac8dbf 100644
--- a/tests/integration/validation/test_request_validators.py
+++ b/tests/integration/validation/test_request_validators.py
@@ -106,6 +106,7 @@ def test_media_type_not_found(self, request_validator):
availableMimetypes=[
"application/json",
"application/x-www-form-urlencoded",
+ "multipart/form-data",
"text/plain",
],
)
diff --git a/tests/unit/templating/test_templating_util.py b/tests/unit/templating/test_templating_util.py
index b268e4f0..815f6cb0 100644
--- a/tests/unit/templating/test_templating_util.py
+++ b/tests/unit/templating/test_templating_util.py
@@ -29,6 +29,7 @@ def test_exact(self):
[
("/{test_id}/test", {"test_id": "test"}),
("/{test.id}/test", {"test.id": "test"}),
+ ("/{test-id}/test", {"test-id": "test"}),
],
)
def test_chars_valid(self, path_pattern, expected):
@@ -49,7 +50,6 @@ def test_chars_valid(self, path_pattern, expected):
"path_pattern,expected",
[
("/{test~id}/test", {"test~id": "test"}),
- ("/{test-id}/test", {"test-id": "test"}),
],
)
def test_special_chars_valid(self, path_pattern, expected):
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy