Schema-First 0.4.0__tar.gz → 0.11.0__tar.gz
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.
- {schema_first-0.4.0/src/Schema_First.egg-info → schema_first-0.11.0}/PKG-INFO +11 -11
- {schema_first-0.4.0 → schema_first-0.11.0}/pyproject.toml +11 -11
- {schema_first-0.4.0 → schema_first-0.11.0/src/Schema_First.egg-info}/PKG-INFO +11 -11
- {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/SOURCES.txt +10 -5
- schema_first-0.11.0/src/Schema_First.egg-info/requires.txt +12 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/__init__.py +2 -2
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/base.py +8 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/constants.py +4 -1
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/fields.py +2 -2
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +2 -2
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/contact_schema.py → schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/contact_object_schema.py +1 -1
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/example_object_schema.py +21 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/external_docs_object_schema.py +13 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/info_object_schema.py +18 -0
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/license_schema.py → schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/license_object_schema.py +1 -1
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/media_type_object_schema.py +3 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/openapi_object_schema.py +57 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +21 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/parameter_object_schema.py +17 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +21 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +7 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +2 -3
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +242 -0
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/server_schema.py → schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/server_object_schema.py +2 -3
- schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/tag_object_schema.py +15 -0
- schema_first-0.11.0/src/schema_first/openapi/schemas/validators.py +29 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/specification/__init__.py +46 -8
- schema_first-0.11.0/tests/test_specification.py +43 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/tests/test_validator.py +2 -0
- schema_first-0.4.0/src/Schema_First.egg-info/requires.txt +0 -12
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -21
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -12
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +0 -9
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -8
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -25
- schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +0 -120
- schema_first-0.4.0/tests/test_specification.py +0 -33
- {schema_first-0.4.0 → schema_first-0.11.0}/LICENSE +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/README.md +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/setup.cfg +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/top_level.txt +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/__init__.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/exceptions.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/__init__.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/exc.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/yaml_loader.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/exc.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/request_body_object_schema.py +0 -0
- {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/server_variable_object_schema.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Schema-First
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: OpenAPI specification validator and converter to Marshmallow schemas.
|
|
5
5
|
Author-email: Konstantin Fadeev <fadeev@legalact.pro>
|
|
6
6
|
License: MIT License
|
|
@@ -30,20 +30,20 @@ Project-URL: repository, https://github.com/flask-pro/schema-first
|
|
|
30
30
|
Classifier: License :: OSI Approved :: MIT License
|
|
31
31
|
Classifier: Operating System :: OS Independent
|
|
32
32
|
Classifier: Programming Language :: Python :: 3
|
|
33
|
-
Requires-Python: >=3.
|
|
33
|
+
Requires-Python: >=3.13
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: marshmallow>=4.0.0
|
|
37
|
-
Requires-Dist: PyYAML>=6.0.
|
|
37
|
+
Requires-Dist: PyYAML>=6.0.3
|
|
38
38
|
Provides-Extra: dev
|
|
39
|
-
Requires-Dist: bandit
|
|
40
|
-
Requires-Dist: build
|
|
41
|
-
Requires-Dist: openapi-spec-validator>=0.
|
|
42
|
-
Requires-Dist: pre-commit
|
|
43
|
-
Requires-Dist: pytest
|
|
44
|
-
Requires-Dist: pytest-cov
|
|
45
|
-
Requires-Dist: python-dotenv
|
|
46
|
-
Requires-Dist: twine==6.
|
|
39
|
+
Requires-Dist: bandit>=1.9.3; extra == "dev"
|
|
40
|
+
Requires-Dist: build>=1.4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: openapi-spec-validator>=0.7.1; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: python-dotenv>=1.2.1; extra == "dev"
|
|
46
|
+
Requires-Dist: twine==6.2.0; extra == "dev"
|
|
47
47
|
Dynamic: license-file
|
|
48
48
|
|
|
49
49
|
# Schema-First
|
|
@@ -13,25 +13,25 @@ classifiers = [
|
|
|
13
13
|
]
|
|
14
14
|
dependencies = [
|
|
15
15
|
'marshmallow>=4.0.0',
|
|
16
|
-
'PyYAML>=6.0.
|
|
16
|
+
'PyYAML>=6.0.3'
|
|
17
17
|
]
|
|
18
18
|
description = "OpenAPI specification validator and converter to Marshmallow schemas."
|
|
19
19
|
license = {file = "LICENSE"}
|
|
20
20
|
name = "Schema-First"
|
|
21
21
|
readme = "README.md"
|
|
22
|
-
requires-python = ">=3.
|
|
23
|
-
version = "0.
|
|
22
|
+
requires-python = ">=3.13"
|
|
23
|
+
version = "0.11.0"
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
dev = [
|
|
27
|
-
"bandit
|
|
28
|
-
"build
|
|
29
|
-
'openapi-spec-validator>=0.
|
|
30
|
-
"pre-commit
|
|
31
|
-
"pytest
|
|
32
|
-
"pytest-cov
|
|
33
|
-
"python-dotenv
|
|
34
|
-
"twine==6.
|
|
27
|
+
"bandit>=1.9.3",
|
|
28
|
+
"build>=1.4.0",
|
|
29
|
+
'openapi-spec-validator>=0.7.1',
|
|
30
|
+
"pre-commit>=4.5.1",
|
|
31
|
+
"pytest>=9.0.2",
|
|
32
|
+
"pytest-cov>=7.0.0",
|
|
33
|
+
"python-dotenv>=1.2.1",
|
|
34
|
+
"twine==6.2.0"
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.urls]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Schema-First
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: OpenAPI specification validator and converter to Marshmallow schemas.
|
|
5
5
|
Author-email: Konstantin Fadeev <fadeev@legalact.pro>
|
|
6
6
|
License: MIT License
|
|
@@ -30,20 +30,20 @@ Project-URL: repository, https://github.com/flask-pro/schema-first
|
|
|
30
30
|
Classifier: License :: OSI Approved :: MIT License
|
|
31
31
|
Classifier: Operating System :: OS Independent
|
|
32
32
|
Classifier: Programming Language :: Python :: 3
|
|
33
|
-
Requires-Python: >=3.
|
|
33
|
+
Requires-Python: >=3.13
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
35
35
|
License-File: LICENSE
|
|
36
36
|
Requires-Dist: marshmallow>=4.0.0
|
|
37
|
-
Requires-Dist: PyYAML>=6.0.
|
|
37
|
+
Requires-Dist: PyYAML>=6.0.3
|
|
38
38
|
Provides-Extra: dev
|
|
39
|
-
Requires-Dist: bandit
|
|
40
|
-
Requires-Dist: build
|
|
41
|
-
Requires-Dist: openapi-spec-validator>=0.
|
|
42
|
-
Requires-Dist: pre-commit
|
|
43
|
-
Requires-Dist: pytest
|
|
44
|
-
Requires-Dist: pytest-cov
|
|
45
|
-
Requires-Dist: python-dotenv
|
|
46
|
-
Requires-Dist: twine==6.
|
|
39
|
+
Requires-Dist: bandit>=1.9.3; extra == "dev"
|
|
40
|
+
Requires-Dist: build>=1.4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: openapi-spec-validator>=0.7.1; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == "dev"
|
|
45
|
+
Requires-Dist: python-dotenv>=1.2.1; extra == "dev"
|
|
46
|
+
Requires-Dist: twine==6.2.0; extra == "dev"
|
|
47
47
|
Dynamic: license-file
|
|
48
48
|
|
|
49
49
|
# Schema-First
|
|
@@ -17,20 +17,25 @@ src/schema_first/openapi/schemas/__init__.py
|
|
|
17
17
|
src/schema_first/openapi/schemas/base.py
|
|
18
18
|
src/schema_first/openapi/schemas/constants.py
|
|
19
19
|
src/schema_first/openapi/schemas/fields.py
|
|
20
|
+
src/schema_first/openapi/schemas/validators.py
|
|
20
21
|
src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py
|
|
21
|
-
src/schema_first/openapi/schemas/v3_1_1/
|
|
22
|
-
src/schema_first/openapi/schemas/v3_1_1/
|
|
23
|
-
src/schema_first/openapi/schemas/v3_1_1/
|
|
22
|
+
src/schema_first/openapi/schemas/v3_1_1/contact_object_schema.py
|
|
23
|
+
src/schema_first/openapi/schemas/v3_1_1/example_object_schema.py
|
|
24
|
+
src/schema_first/openapi/schemas/v3_1_1/external_docs_object_schema.py
|
|
25
|
+
src/schema_first/openapi/schemas/v3_1_1/info_object_schema.py
|
|
26
|
+
src/schema_first/openapi/schemas/v3_1_1/license_object_schema.py
|
|
24
27
|
src/schema_first/openapi/schemas/v3_1_1/media_type_object_schema.py
|
|
28
|
+
src/schema_first/openapi/schemas/v3_1_1/openapi_object_schema.py
|
|
25
29
|
src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py
|
|
30
|
+
src/schema_first/openapi/schemas/v3_1_1/parameter_object_schema.py
|
|
26
31
|
src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py
|
|
27
32
|
src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py
|
|
28
33
|
src/schema_first/openapi/schemas/v3_1_1/request_body_object_schema.py
|
|
29
34
|
src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py
|
|
30
|
-
src/schema_first/openapi/schemas/v3_1_1/root_schema.py
|
|
31
35
|
src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py
|
|
32
|
-
src/schema_first/openapi/schemas/v3_1_1/
|
|
36
|
+
src/schema_first/openapi/schemas/v3_1_1/server_object_schema.py
|
|
33
37
|
src/schema_first/openapi/schemas/v3_1_1/server_variable_object_schema.py
|
|
38
|
+
src/schema_first/openapi/schemas/v3_1_1/tag_object_schema.py
|
|
34
39
|
src/schema_first/specification/__init__.py
|
|
35
40
|
tests/test_specification.py
|
|
36
41
|
tests/test_validator.py
|
|
@@ -5,7 +5,7 @@ from marshmallow import ValidationError
|
|
|
5
5
|
|
|
6
6
|
from ..loaders.yaml_loader import load_from_yaml
|
|
7
7
|
from .exc import OpenAPIValidationError
|
|
8
|
-
from .schemas.v3_1_1.
|
|
8
|
+
from .schemas.v3_1_1.openapi_object_schema import OpenAPIObjectSchema
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class OpenAPI:
|
|
@@ -15,6 +15,6 @@ class OpenAPI:
|
|
|
15
15
|
|
|
16
16
|
def load(self) -> dict:
|
|
17
17
|
try:
|
|
18
|
-
return
|
|
18
|
+
return OpenAPIObjectSchema().load(self.raw_spec)
|
|
19
19
|
except ValidationError as e:
|
|
20
20
|
raise OpenAPIValidationError(f'\n{pformat(e.messages)}')
|
|
@@ -3,6 +3,9 @@ from marshmallow import Schema
|
|
|
3
3
|
from marshmallow import validates_schema
|
|
4
4
|
from marshmallow import ValidationError
|
|
5
5
|
|
|
6
|
+
from .fields import DESCRIPTION_FIELD
|
|
7
|
+
from .fields import SUMMARY_FIELD
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
class BaseSchema(Schema):
|
|
8
11
|
class Meta:
|
|
@@ -18,3 +21,8 @@ class BaseSchema(Schema):
|
|
|
18
21
|
f"If there is a <'ref'> field, then only <{ALLOWED_FIELDS}>,"
|
|
19
22
|
f" but set <{ALL_FIELDS}>"
|
|
20
23
|
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DocStringFields(Schema):
|
|
27
|
+
summary = SUMMARY_FIELD
|
|
28
|
+
description = DESCRIPTION_FIELD
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
OPENAPI_VERSION_3_2 = '3.2'
|
|
2
2
|
TYPES = ('array', 'boolean', 'integer', 'number', 'object', 'string')
|
|
3
3
|
FORMATS = ('uuid', 'date-time', 'date', 'time', 'email', 'ipv4', 'ipv6', 'uri', 'binary')
|
|
4
|
+
INT_FORMATS = ('int32', 'int64')
|
|
5
|
+
FLOAT_FORMATS = ('float', 'double')
|
|
4
6
|
RE_VERSION = r'^[0-9]+.[0-9]+.[0-9]+$'
|
|
5
7
|
RE_SERVER_URL = r'^((http|https)://)|(/)*$'
|
|
8
|
+
LOCATION_PARAMETER = ('query', 'querystring', 'header', 'path', 'cookie')
|
|
@@ -3,8 +3,8 @@ from marshmallow import validate
|
|
|
3
3
|
|
|
4
4
|
ENDPOINT_FIELD = fields.String(required=True, validate=validate.Regexp(r'^[/][0-9a-z-{}/]*[^/]$'))
|
|
5
5
|
HTTP_CODE_FIELD = fields.String(required=True, validate=validate.Regexp(r'^[1-5]{1}\d{2}|default$'))
|
|
6
|
-
SUMMARY_FIELD = fields.String()
|
|
7
|
-
DESCRIPTION_FIELD = fields.String()
|
|
6
|
+
SUMMARY_FIELD = fields.String(validate=validate.Length(min=1, max=150))
|
|
7
|
+
DESCRIPTION_FIELD = fields.String(validate=validate.Length(min=1))
|
|
8
8
|
REQUIRED_DESCRIPTION_FIELD = fields.String(required=True)
|
|
9
9
|
MEDIA_TYPE_FIELD = fields.String(required=True)
|
|
10
10
|
REF_FIELD = fields.String(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from marshmallow import fields
|
|
2
2
|
|
|
3
3
|
from ..base import BaseSchema
|
|
4
|
-
from .responses_object_schema import
|
|
4
|
+
from .responses_object_schema import ResponseObjectSchema
|
|
5
5
|
from .schema_object_schema import SchemaObjectSchema
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ComponentsObjectSchema(BaseSchema):
|
|
9
9
|
responses = fields.Dict(
|
|
10
|
-
keys=fields.String(), values=fields.Nested(
|
|
10
|
+
keys=fields.String(), values=fields.Nested(ResponseObjectSchema, required=True)
|
|
11
11
|
)
|
|
12
12
|
schemas = fields.Dict(
|
|
13
13
|
keys=fields.String(), values=fields.Nested(SchemaObjectSchema, required=True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import validate
|
|
3
|
+
from marshmallow import validates_schema
|
|
4
|
+
from marshmallow import ValidationError
|
|
5
|
+
|
|
6
|
+
from ..base import BaseSchema
|
|
7
|
+
from ..base import DocStringFields
|
|
8
|
+
from .schema_object_schema import SchemaObjectSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExampleObjectSchema(DocStringFields, BaseSchema):
|
|
12
|
+
dataValue = fields.Nested(SchemaObjectSchema)
|
|
13
|
+
serializedValue = fields.String(validate=validate.Length(min=1))
|
|
14
|
+
externalValue = fields.String(validate=validate.Length(min=1))
|
|
15
|
+
|
|
16
|
+
@validates_schema
|
|
17
|
+
def validate_exclusive(self, data, **kwargs) -> None:
|
|
18
|
+
if 'serializedValue' in data and 'externalValue' in data:
|
|
19
|
+
raise ValidationError(
|
|
20
|
+
'The <serializedValue> field is mutually exclusive of the <externalValue> field.'
|
|
21
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import validate
|
|
3
|
+
|
|
4
|
+
from ..base import BaseSchema
|
|
5
|
+
from ..constants import RE_SERVER_URL
|
|
6
|
+
from ..fields import DESCRIPTION_FIELD
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExternalDocsObjectSchema(BaseSchema):
|
|
10
|
+
url = fields.String(
|
|
11
|
+
required=True, validate=[validate.Regexp(RE_SERVER_URL), validate.Length(min=1)]
|
|
12
|
+
)
|
|
13
|
+
description = DESCRIPTION_FIELD
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import validate
|
|
3
|
+
|
|
4
|
+
from ..base import BaseSchema
|
|
5
|
+
from ..base import DocStringFields
|
|
6
|
+
from ..constants import RE_VERSION
|
|
7
|
+
from .contact_object_schema import ContactObjectSchema
|
|
8
|
+
from .license_object_schema import LicenseObjectSchema
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InfoObjectSchema(DocStringFields, BaseSchema):
|
|
12
|
+
title = fields.String(required=True)
|
|
13
|
+
version = fields.String(required=True, validate=validate.Regexp(RE_VERSION))
|
|
14
|
+
|
|
15
|
+
termsOfService = fields.String()
|
|
16
|
+
|
|
17
|
+
contact = fields.Nested(ContactObjectSchema)
|
|
18
|
+
license = fields.Nested(LicenseObjectSchema)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from marshmallow import fields
|
|
2
2
|
|
|
3
3
|
from ..base import BaseSchema
|
|
4
|
+
from .example_object_schema import ExampleObjectSchema
|
|
4
5
|
from .schema_object_schema import SchemaObjectSchema
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class MediaTypeObjectSchema(BaseSchema):
|
|
8
9
|
schema = fields.Nested(SchemaObjectSchema)
|
|
10
|
+
itemSchema = fields.Nested(SchemaObjectSchema)
|
|
11
|
+
examples = fields.Dict(keys=fields.String(), values=fields.Nested(ExampleObjectSchema))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import post_load
|
|
3
|
+
from marshmallow import validates_schema
|
|
4
|
+
from marshmallow import ValidationError
|
|
5
|
+
|
|
6
|
+
from ..base import BaseSchema
|
|
7
|
+
from ..constants import OPENAPI_VERSION_3_2
|
|
8
|
+
from ..fields import ENDPOINT_FIELD
|
|
9
|
+
from ..validators import VersionMatch
|
|
10
|
+
from .components_object_schema import ComponentsObjectSchema
|
|
11
|
+
from .external_docs_object_schema import ExternalDocsObjectSchema
|
|
12
|
+
from .info_object_schema import InfoObjectSchema
|
|
13
|
+
from .path_item_object_schema import PathItemObjectSchema
|
|
14
|
+
from .server_object_schema import ServerObjectSchema
|
|
15
|
+
from .tag_object_schema import TagObjectSchema
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenAPIObjectSchema(BaseSchema):
|
|
19
|
+
openapi = fields.String(required=True, validate=VersionMatch(OPENAPI_VERSION_3_2))
|
|
20
|
+
info = fields.Nested(InfoObjectSchema, required=True)
|
|
21
|
+
paths = fields.Dict(
|
|
22
|
+
required=True,
|
|
23
|
+
keys=ENDPOINT_FIELD,
|
|
24
|
+
values=fields.Nested(PathItemObjectSchema, required=True),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
jsonSchemaDialect = fields.URL()
|
|
28
|
+
|
|
29
|
+
servers = fields.Nested(ServerObjectSchema, many=True)
|
|
30
|
+
components = fields.Nested(ComponentsObjectSchema)
|
|
31
|
+
tags = fields.Nested(TagObjectSchema, many=True)
|
|
32
|
+
externalDocs = fields.Nested(ExternalDocsObjectSchema)
|
|
33
|
+
|
|
34
|
+
@validates_schema
|
|
35
|
+
def validate_tags(self, data, **kwargs):
|
|
36
|
+
if tags := data.get('tags'):
|
|
37
|
+
names = []
|
|
38
|
+
parents = []
|
|
39
|
+
|
|
40
|
+
for tag in tags:
|
|
41
|
+
names.append(tag['name'])
|
|
42
|
+
|
|
43
|
+
if parent := tag.get('parent'):
|
|
44
|
+
parents.append(parent)
|
|
45
|
+
|
|
46
|
+
parents_not_in_names = [parent for parent in parents if parent not in names]
|
|
47
|
+
if parents_not_in_names:
|
|
48
|
+
raise ValidationError(f'Parents <{names}> not exist in names tags <{names}>.')
|
|
49
|
+
|
|
50
|
+
@post_load
|
|
51
|
+
def validate_path_parameter(self, data, **kwargs) -> None:
|
|
52
|
+
endpoints = data['paths'].keys()
|
|
53
|
+
for endpoint in endpoints:
|
|
54
|
+
if '{' in endpoint or '}' in endpoint:
|
|
55
|
+
raise NotImplementedError(
|
|
56
|
+
'Need check path-parameters template in "parameters" key.'
|
|
57
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
|
|
3
|
+
from ..base import BaseSchema
|
|
4
|
+
from ..base import DocStringFields
|
|
5
|
+
from ..fields import HTTP_CODE_FIELD
|
|
6
|
+
from .external_docs_object_schema import ExternalDocsObjectSchema
|
|
7
|
+
from .parameter_object_schema import ParameterObjectSchema
|
|
8
|
+
from .request_body_object_schema import RequestBodyObject
|
|
9
|
+
from .responses_object_schema import ResponseObjectSchema
|
|
10
|
+
from .server_object_schema import ServerObjectSchema
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OperationObjectSchema(DocStringFields, BaseSchema):
|
|
14
|
+
tags = fields.List(fields.String)
|
|
15
|
+
externalDocs = fields.Nested(ExternalDocsObjectSchema)
|
|
16
|
+
operationId = fields.String()
|
|
17
|
+
requestBody = fields.Nested(RequestBodyObject)
|
|
18
|
+
responses = fields.Dict(keys=HTTP_CODE_FIELD, values=fields.Nested(ResponseObjectSchema))
|
|
19
|
+
deprecated = fields.Boolean()
|
|
20
|
+
servers = fields.Nested(ServerObjectSchema, many=True)
|
|
21
|
+
parameters = fields.Nested(ParameterObjectSchema, many=True)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import validate
|
|
3
|
+
|
|
4
|
+
from ..base import BaseSchema
|
|
5
|
+
from ..constants import LOCATION_PARAMETER
|
|
6
|
+
from ..fields import DESCRIPTION_FIELD
|
|
7
|
+
from .example_object_schema import ExampleObjectSchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParameterObjectSchema(BaseSchema):
|
|
11
|
+
name = fields.String(required=True)
|
|
12
|
+
in_ = fields.String(required=True, data_key='in', validate=validate.OneOf(LOCATION_PARAMETER))
|
|
13
|
+
|
|
14
|
+
description = DESCRIPTION_FIELD
|
|
15
|
+
required = fields.Boolean()
|
|
16
|
+
deprecated = fields.Boolean()
|
|
17
|
+
examples = fields.Dict(keys=fields.String(), values=fields.Nested(ExampleObjectSchema))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
|
|
3
|
+
from ..base import BaseSchema
|
|
4
|
+
from ..base import DocStringFields
|
|
5
|
+
from .operation_object_schema import OperationObjectSchema
|
|
6
|
+
from .parameter_object_schema import ParameterObjectSchema
|
|
7
|
+
from .server_object_schema import ServerObjectSchema
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PathItemObjectSchema(DocStringFields, BaseSchema):
|
|
11
|
+
get = fields.Nested(OperationObjectSchema)
|
|
12
|
+
put = fields.Nested(OperationObjectSchema)
|
|
13
|
+
post = fields.Nested(OperationObjectSchema)
|
|
14
|
+
delete = fields.Nested(OperationObjectSchema)
|
|
15
|
+
options = fields.Nested(OperationObjectSchema)
|
|
16
|
+
head = fields.Nested(OperationObjectSchema)
|
|
17
|
+
patch = fields.Nested(OperationObjectSchema)
|
|
18
|
+
trace = fields.Nested(OperationObjectSchema)
|
|
19
|
+
query = fields.Nested(OperationObjectSchema)
|
|
20
|
+
servers = fields.Nested(ServerObjectSchema, many=True)
|
|
21
|
+
parameters = fields.Nested(ParameterObjectSchema, many=True)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from marshmallow import fields
|
|
2
2
|
|
|
3
3
|
from ..base import BaseSchema
|
|
4
|
-
from ..
|
|
4
|
+
from ..base import DocStringFields
|
|
5
5
|
from ..fields import MEDIA_TYPE_FIELD
|
|
6
6
|
from .media_type_object_schema import MediaTypeObjectSchema
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
10
|
-
description = DESCRIPTION_FIELD
|
|
9
|
+
class ResponseObjectSchema(DocStringFields, BaseSchema):
|
|
11
10
|
content = fields.Dict(keys=MEDIA_TYPE_FIELD, values=fields.Nested(MediaTypeObjectSchema))
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
import math
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
from marshmallow import fields
|
|
7
|
+
from marshmallow import types
|
|
8
|
+
from marshmallow import validate
|
|
9
|
+
from marshmallow import validates
|
|
10
|
+
from marshmallow import validates_schema
|
|
11
|
+
from marshmallow import ValidationError
|
|
12
|
+
|
|
13
|
+
from schema_first.openapi.schemas.constants import FLOAT_FORMATS
|
|
14
|
+
from schema_first.openapi.schemas.constants import INT_FORMATS
|
|
15
|
+
|
|
16
|
+
from ..base import BaseSchema
|
|
17
|
+
from ..base import DocStringFields
|
|
18
|
+
from ..constants import FORMATS
|
|
19
|
+
from ..constants import TYPES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BaseSchemaField(DocStringFields, BaseSchema):
|
|
23
|
+
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
24
|
+
nullable = fields.Boolean()
|
|
25
|
+
|
|
26
|
+
@validates_schema
|
|
27
|
+
def validate_default_via_format(self, data, **kwargs):
|
|
28
|
+
if 'default' in data and 'format' in data:
|
|
29
|
+
error = format_schemas[data['format']]().validate({'default': data['default']})
|
|
30
|
+
if error:
|
|
31
|
+
raise ValidationError(str(error))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FormatBinarySchema(BaseSchema):
|
|
35
|
+
default = fields.String()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FormatEmailSchema(BaseSchema):
|
|
39
|
+
default = fields.Email()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class FormatDateSchema(BaseSchema):
|
|
43
|
+
default = fields.Date()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FormatDateTimeSchema(BaseSchema):
|
|
47
|
+
default = fields.AwareDateTime(format='iso', default_timezone=None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class FormatIPv4Schema(BaseSchema):
|
|
51
|
+
default = fields.IPv4()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FormatIPv6Schema(BaseSchema):
|
|
55
|
+
default = fields.IPv6()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FormatTimeSchema(BaseSchema):
|
|
59
|
+
default = fields.Time(format='iso')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FormatURISchema(BaseSchema):
|
|
63
|
+
default = fields.URL()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FormatUUIDSchema(BaseSchema):
|
|
67
|
+
default = fields.UUID()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FormatINT32Schema(BaseSchema):
|
|
71
|
+
default = fields.Integer(validate=validate.Range(min=-2_147_483_648, max=2_147_483_647))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FormatINT64Schema(BaseSchema):
|
|
75
|
+
default = fields.Integer(
|
|
76
|
+
validate=validate.Range(min=-9_223_372_036_854_775_808, max=9_223_372_036_854_775_807)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class FormatFloatSchema(BaseSchema):
|
|
81
|
+
default = fields.Float(validate=validate.Range(min=3.4e-38, max=3.4e38))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class FormatDoubleSchema(BaseSchema):
|
|
85
|
+
default = fields.Float(validate=validate.Range(min=1.7e-308, max=1.7e308))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class StringFieldSchema(BaseSchemaField):
|
|
89
|
+
format = fields.String(validate=validate.OneOf(FORMATS))
|
|
90
|
+
minLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
91
|
+
maxLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
92
|
+
pattern = fields.String()
|
|
93
|
+
default = fields.String()
|
|
94
|
+
|
|
95
|
+
@validates('pattern')
|
|
96
|
+
def validate_pattern(self, value: str, data_key: str) -> None:
|
|
97
|
+
try:
|
|
98
|
+
re.compile(value)
|
|
99
|
+
except re.PatternError as e:
|
|
100
|
+
raise ValidationError(f"Pattern <{value}> is error <{repr(e)}>.")
|
|
101
|
+
|
|
102
|
+
@validates_schema
|
|
103
|
+
def validate_default(self, data, **kwargs):
|
|
104
|
+
if 'default' in data and 'pattern' in data:
|
|
105
|
+
result = re.match(data['pattern'], data['default'])
|
|
106
|
+
if result is None:
|
|
107
|
+
raise ValidationError(f'<{data["default"]}> does not match <{data["pattern"]}>')
|
|
108
|
+
|
|
109
|
+
@validates_schema
|
|
110
|
+
def validate_length(self, data, **kwargs):
|
|
111
|
+
if 'minLength' in data and 'maxLength' in data:
|
|
112
|
+
if data['minLength'] > data['maxLength']:
|
|
113
|
+
raise ValidationError(
|
|
114
|
+
f'<{data["minLength"]}> cannot be greater than <{data["maxLength"]}>'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ObjectFieldSchema(BaseSchemaField):
|
|
119
|
+
required = fields.List(fields.String())
|
|
120
|
+
additionalProperties = fields.Boolean()
|
|
121
|
+
|
|
122
|
+
properties = fields.Dict(
|
|
123
|
+
keys=fields.String(required=True, validate=validate.Length(min=1)),
|
|
124
|
+
values=fields.Nested(lambda: SchemaObjectSchema()),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@validates_schema
|
|
128
|
+
def validate_required(self, data, **kwargs):
|
|
129
|
+
if 'required' in data:
|
|
130
|
+
for field_name in data['required']:
|
|
131
|
+
if field_name not in data['properties']:
|
|
132
|
+
raise ValidationError(
|
|
133
|
+
f'Required field <{field_name}> not in <data["properties"]>'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class BooleanFieldSchema(BaseSchemaField):
|
|
138
|
+
default = fields.Boolean(truthy=[True], falsy=[False])
|
|
139
|
+
|
|
140
|
+
@validates_schema
|
|
141
|
+
def validate_default(self, data, **kwargs):
|
|
142
|
+
if 'default' in data:
|
|
143
|
+
if not isinstance(data['default'], bool):
|
|
144
|
+
raise ValidationError(f'<{data["default"]}> is not boolean.')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class NumberFieldSchema(BaseSchemaField):
|
|
148
|
+
format = fields.String(validate=validate.OneOf(FLOAT_FORMATS))
|
|
149
|
+
minimum = fields.Float()
|
|
150
|
+
maximum = fields.Float()
|
|
151
|
+
exclusiveMinimum = fields.Float()
|
|
152
|
+
exclusiveMaximum = fields.Float()
|
|
153
|
+
multipleOf = fields.Float(validate=[validate.Range(min=0, min_inclusive=False)])
|
|
154
|
+
default = fields.Float()
|
|
155
|
+
|
|
156
|
+
@validates_schema
|
|
157
|
+
def validate_default(self, data, **kwargs):
|
|
158
|
+
if 'default' in data:
|
|
159
|
+
default = data['default']
|
|
160
|
+
|
|
161
|
+
minimum = data.get('minimum', -math.inf)
|
|
162
|
+
maximum = data.get('maximum', math.inf)
|
|
163
|
+
if not minimum <= default <= maximum:
|
|
164
|
+
raise ValidationError(
|
|
165
|
+
f'Value <{default}> must be greater than or equal to <{minimum}>'
|
|
166
|
+
f' and less than or equal to <{maximum}>.'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
exclusive_minimum = data.get('exclusiveMinimum', -math.inf)
|
|
170
|
+
exclusive_maximum = data.get('exclusiveMaximum', math.inf)
|
|
171
|
+
if not exclusive_minimum < default < exclusive_maximum:
|
|
172
|
+
raise ValidationError(
|
|
173
|
+
f'Value <{default}> must be greater to <{minimum}> and less to <{maximum}>.'
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
@validates_schema
|
|
177
|
+
def validate_min_max(self, data, **kwargs):
|
|
178
|
+
if 'exclusiveMinimum' in data and 'exclusiveMaximum' in data:
|
|
179
|
+
exclusive_min = data['exclusiveMinimum']
|
|
180
|
+
exclusive_max = data['exclusiveMaximum']
|
|
181
|
+
if exclusive_min > exclusive_max:
|
|
182
|
+
raise ValidationError(f'<{exclusive_min}> cannot be greater than <{exclusive_max}>')
|
|
183
|
+
|
|
184
|
+
if 'minimum' in data and 'maximum' in data:
|
|
185
|
+
minimum = data['minimum']
|
|
186
|
+
maximum = data['maximum']
|
|
187
|
+
if minimum > maximum:
|
|
188
|
+
raise ValidationError(f'<{minimum}> cannot be greater than <{maximum}>')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class IntegerFieldSchema(NumberFieldSchema):
|
|
192
|
+
format = fields.String(validate=validate.OneOf(INT_FORMATS))
|
|
193
|
+
minimum = fields.Integer()
|
|
194
|
+
maximum = fields.Integer()
|
|
195
|
+
exclusiveMinimum = fields.Integer()
|
|
196
|
+
exclusiveMaximum = fields.Integer()
|
|
197
|
+
multipleOf = fields.Integer(validate=[validate.Range(min=0, min_inclusive=False)])
|
|
198
|
+
default = fields.Integer()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
field_schemas = {
|
|
202
|
+
'boolean': BooleanFieldSchema,
|
|
203
|
+
'integer': IntegerFieldSchema,
|
|
204
|
+
'number': NumberFieldSchema,
|
|
205
|
+
'object': ObjectFieldSchema,
|
|
206
|
+
'string': StringFieldSchema,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
format_schemas = {
|
|
210
|
+
'binary': FormatBinarySchema,
|
|
211
|
+
'date': FormatDateSchema,
|
|
212
|
+
'date-time': FormatDateTimeSchema,
|
|
213
|
+
'email': FormatEmailSchema,
|
|
214
|
+
'ipv4': FormatIPv4Schema,
|
|
215
|
+
'ipv6': FormatIPv6Schema,
|
|
216
|
+
'time': FormatTimeSchema,
|
|
217
|
+
'uri': FormatURISchema,
|
|
218
|
+
'uuid': FormatUUIDSchema,
|
|
219
|
+
'int32': FormatINT32Schema,
|
|
220
|
+
'int64': FormatINT64Schema,
|
|
221
|
+
'float': FormatFloatSchema,
|
|
222
|
+
'double': FormatDoubleSchema,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class SchemaObjectSchema(BaseSchemaField):
|
|
227
|
+
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
228
|
+
|
|
229
|
+
def load(
|
|
230
|
+
self,
|
|
231
|
+
data: Mapping[str, typing.Any] | Sequence[Mapping[str, typing.Any]],
|
|
232
|
+
*,
|
|
233
|
+
many: bool | None = None,
|
|
234
|
+
partial: bool | types.StrSequenceOrSet | None = None,
|
|
235
|
+
unknown: types.UnknownOption | None = None,
|
|
236
|
+
):
|
|
237
|
+
try:
|
|
238
|
+
return field_schemas[data['type']]().load(
|
|
239
|
+
data, many=many, partial=partial, unknown=unknown
|
|
240
|
+
)
|
|
241
|
+
except KeyError:
|
|
242
|
+
raise ValidationError(f'Data type in <{data}> not exist.')
|
|
@@ -7,13 +7,12 @@ from ..fields import DESCRIPTION_FIELD
|
|
|
7
7
|
from .server_variable_object_schema import ServerVariableObjectSchema
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
class
|
|
10
|
+
class ServerObjectSchema(BaseSchema):
|
|
11
11
|
url = fields.String(
|
|
12
12
|
required=True, validate=[validate.Regexp(RE_SERVER_URL), validate.Length(min=1)]
|
|
13
13
|
)
|
|
14
|
-
|
|
15
14
|
description = DESCRIPTION_FIELD
|
|
16
|
-
|
|
15
|
+
name = fields.String(validate=[validate.Length(min=1)])
|
|
17
16
|
variables = fields.Dict(
|
|
18
17
|
keys=fields.String(), values=fields.Nested(ServerVariableObjectSchema, required=True)
|
|
19
18
|
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import validate
|
|
3
|
+
|
|
4
|
+
from ..base import BaseSchema
|
|
5
|
+
from ..base import DocStringFields
|
|
6
|
+
from .external_docs_object_schema import ExternalDocsObjectSchema
|
|
7
|
+
|
|
8
|
+
kinds = ['audience', 'badge', 'nav']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TagObjectSchema(DocStringFields, BaseSchema):
|
|
12
|
+
name = fields.String(required=True)
|
|
13
|
+
externalDocs = fields.Nested(ExternalDocsObjectSchema)
|
|
14
|
+
parent = fields.String()
|
|
15
|
+
kind = fields.String(validate=validate.OneOf(choices=kinds))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from marshmallow.exceptions import ValidationError
|
|
2
|
+
from marshmallow.validate import Validator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class VersionMatch(Validator):
|
|
6
|
+
"""Version specification validator check minor and major parts from string.
|
|
7
|
+
The patch part is not meaningful.
|
|
8
|
+
|
|
9
|
+
:param comparable: The object to compare to.
|
|
10
|
+
:param error: Error message to raise in case of a validation error.
|
|
11
|
+
Can be interpolated with `{input}` and `{other}`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
default_message = 'Version {input} must be start with {other}.'
|
|
15
|
+
|
|
16
|
+
def __init__(self, comparable, *, error: str | None = None):
|
|
17
|
+
self.comparable = comparable
|
|
18
|
+
self.error: str = error or self.default_message
|
|
19
|
+
|
|
20
|
+
def _repr_args(self) -> str:
|
|
21
|
+
return f"comparable={self.comparable!r}"
|
|
22
|
+
|
|
23
|
+
def _format_error(self, value: str) -> str:
|
|
24
|
+
return self.error.format(input=value, other=self.comparable)
|
|
25
|
+
|
|
26
|
+
def __call__(self, value: str) -> str:
|
|
27
|
+
if not value.startswith(f'{self.comparable}.'):
|
|
28
|
+
raise ValidationError(self._format_error(value))
|
|
29
|
+
return value
|
|
@@ -50,8 +50,7 @@ class Specification:
|
|
|
50
50
|
if schema['type'] in ['integer', 'number']:
|
|
51
51
|
validators.append(validate.Range(min=schema.get('minimum'), max=schema.get('maximum')))
|
|
52
52
|
|
|
53
|
-
required_values
|
|
54
|
-
if required_values:
|
|
53
|
+
if required_values := schema.get('enum'):
|
|
55
54
|
validators.append(validate.OneOf(required_values))
|
|
56
55
|
|
|
57
56
|
return validators
|
|
@@ -71,11 +70,48 @@ class Specification:
|
|
|
71
70
|
initialized_schema.required = required
|
|
72
71
|
return initialized_schema
|
|
73
72
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
raise NotImplementedError(
|
|
73
|
+
def _convert_boolean_field(self, field_schema: dict, required: bool = False):
|
|
74
|
+
try:
|
|
75
|
+
schema = FIELDS_VIA_TYPES[field_schema['type']]
|
|
76
|
+
except KeyError:
|
|
77
|
+
raise NotImplementedError(
|
|
78
|
+
f'Schema <{field_schema}> for type <{field_schema["type"]}> not implemented.'
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
initialized_schema = schema()
|
|
82
|
+
initialized_schema.allow_none = field_schema.get('nullable', False)
|
|
83
|
+
initialized_schema.required = required
|
|
84
|
+
return initialized_schema
|
|
85
|
+
|
|
86
|
+
def _convert_number_field(self, field_schema: dict, required: bool = False):
|
|
87
|
+
try:
|
|
88
|
+
schema = FIELDS_VIA_TYPES[field_schema['type']]
|
|
89
|
+
except KeyError:
|
|
90
|
+
raise NotImplementedError(
|
|
91
|
+
f'Schema <{field_schema}> for type <{field_schema["type"]}> not implemented.'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
initialized_schema = schema()
|
|
95
|
+
initialized_schema.validate = self._make_field_validators(field_schema)
|
|
96
|
+
initialized_schema.allow_none = field_schema.get('nullable', False)
|
|
97
|
+
initialized_schema.required = required
|
|
98
|
+
return initialized_schema
|
|
99
|
+
|
|
100
|
+
def _convert_field_any_type(self, field_schema: dict, required: bool = False):
|
|
101
|
+
field_schema_converters = {
|
|
102
|
+
'string': self._convert_string_field,
|
|
103
|
+
'boolean': self._convert_boolean_field,
|
|
104
|
+
'number': self._convert_number_field,
|
|
105
|
+
'integer': self._convert_number_field,
|
|
106
|
+
}
|
|
107
|
+
try:
|
|
108
|
+
converted_field_schema = field_schema_converters[field_schema['type']](
|
|
109
|
+
field_schema, required=required
|
|
110
|
+
)
|
|
111
|
+
except KeyError:
|
|
112
|
+
raise NotImplementedError(
|
|
113
|
+
f'Schema <{field_schema}> for type <{field_schema["type"]}> not be converted.'
|
|
114
|
+
)
|
|
79
115
|
|
|
80
116
|
return converted_field_schema
|
|
81
117
|
|
|
@@ -104,7 +140,9 @@ class Specification:
|
|
|
104
140
|
def _reassembly_of_schemas(self, obj: Any) -> Any:
|
|
105
141
|
if isinstance(obj, dict):
|
|
106
142
|
for k, v in obj.items():
|
|
107
|
-
|
|
143
|
+
# Checking for object type is needed to skip already resolved schemes.
|
|
144
|
+
# This is necessary because of passing variables by reference in Python.
|
|
145
|
+
if k == 'schema' and isinstance(v, dict):
|
|
108
146
|
obj[k] = self._convert_from_openapi_to_marshmallow_schema(v)
|
|
109
147
|
else:
|
|
110
148
|
self._reassembly_of_schemas(v)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from marshmallow import fields
|
|
4
|
+
from marshmallow import Schema
|
|
5
|
+
from openapi_spec_validator import validate as osv_validate
|
|
6
|
+
from openapi_spec_validator.readers import read_from_filename
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from src.schema_first.openapi import OpenAPI
|
|
10
|
+
from src.schema_first.specification import Specification
|
|
11
|
+
from tests.conftest import tests_dir_abspath
|
|
12
|
+
from tests.utils import get_schema_from_request
|
|
13
|
+
|
|
14
|
+
specs_base_dir = Path(tests_dir_abspath, '_contrib', 'specs')
|
|
15
|
+
spec_file_paths = list(Path(specs_base_dir, 'v3.0').iterdir())
|
|
16
|
+
spec_file_paths.extend(list(Path(specs_base_dir, 'v3.1').iterdir()))
|
|
17
|
+
spec_file_paths.extend(list(Path(specs_base_dir, 'v3.2').iterdir()))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize('fx', ['fx_spec_required', 'fx_spec_full'])
|
|
21
|
+
def test_specification(request, fx_spec_as_file, fx):
|
|
22
|
+
spec_file = fx_spec_as_file(request.getfixturevalue(fx))
|
|
23
|
+
spec = Specification(spec_file)
|
|
24
|
+
|
|
25
|
+
assert isinstance(spec.openapi, OpenAPI)
|
|
26
|
+
assert spec.reassembly_spec is None
|
|
27
|
+
|
|
28
|
+
spec.load()
|
|
29
|
+
|
|
30
|
+
request_schema = get_schema_from_request(spec.reassembly_spec, '/required-endpoint', '200')
|
|
31
|
+
assert isinstance(request_schema(), Schema)
|
|
32
|
+
assert isinstance(request_schema().fields['message'], fields.String)
|
|
33
|
+
assert request_schema().load({'message': 'Valid string'})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.xfail(reason='Schema specification not fully realisation.')
|
|
37
|
+
@pytest.mark.parametrize('file_path', spec_file_paths, ids=map(str, spec_file_paths))
|
|
38
|
+
def test_specification_from_file(file_path):
|
|
39
|
+
spec_as_dict, _ = read_from_filename(file_path)
|
|
40
|
+
osv_validate(spec_as_dict)
|
|
41
|
+
|
|
42
|
+
spec = Specification(file_path)
|
|
43
|
+
spec.load()
|
|
@@ -18,12 +18,14 @@ def test_validator__full(fx_spec_full, fx_spec_as_file):
|
|
|
18
18
|
assert open_api_spec.raw_spec == fx_spec_full
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@pytest.mark.xfail(reason='External validator not support version 3.2.0.')
|
|
21
22
|
def test_validator__required__external_validator(fx_spec_required, fx_spec_as_file):
|
|
22
23
|
spec_file = fx_spec_as_file(fx_spec_required, external_validator=True)
|
|
23
24
|
open_api_spec = OpenAPI(spec_file)
|
|
24
25
|
open_api_spec.load()
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
@pytest.mark.xfail(reason='External validator not support version 3.2.0.')
|
|
27
29
|
def test_validator__full__external_validator(fx_spec_full, fx_spec_as_file):
|
|
28
30
|
spec_file = fx_spec_as_file(fx_spec_full, external_validator=True)
|
|
29
31
|
open_api_spec = OpenAPI(spec_file)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
from marshmallow import validate
|
|
3
|
-
|
|
4
|
-
from ..base import BaseSchema
|
|
5
|
-
from ..constants import RE_VERSION
|
|
6
|
-
from ..fields import DESCRIPTION_FIELD
|
|
7
|
-
from ..fields import SUMMARY_FIELD
|
|
8
|
-
from .contact_schema import ContactSchema
|
|
9
|
-
from .license_schema import LicenseSchema
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class InfoSchema(BaseSchema):
|
|
13
|
-
title = fields.String(required=True)
|
|
14
|
-
version = fields.String(required=True, validate=validate.Regexp(RE_VERSION))
|
|
15
|
-
|
|
16
|
-
summary = SUMMARY_FIELD
|
|
17
|
-
description = DESCRIPTION_FIELD
|
|
18
|
-
terms_of_service = fields.String(data_key='termsOfService')
|
|
19
|
-
|
|
20
|
-
contact = fields.Nested(ContactSchema)
|
|
21
|
-
license = fields.Nested(LicenseSchema)
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
|
|
3
|
-
from ..base import BaseSchema
|
|
4
|
-
from ..fields import HTTP_CODE_FIELD
|
|
5
|
-
from .request_body_object_schema import RequestBodyObject
|
|
6
|
-
from .responses_object_schema import ResponsesObjectSchema
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class OperationObjectSchema(BaseSchema):
|
|
10
|
-
operation_id = fields.String(data_key='operationId')
|
|
11
|
-
requestBody = fields.Nested(RequestBodyObject)
|
|
12
|
-
responses = fields.Dict(keys=HTTP_CODE_FIELD, values=fields.Nested(ResponsesObjectSchema))
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
from schema_first.openapi.schemas._base import BaseSchema
|
|
2
|
-
from schema_first.openapi.schemas._fields import DESCRIPTION_FIELD
|
|
3
|
-
from schema_first.openapi.schemas._fields import REF_FIELD
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ReferenceObjectSchema(BaseSchema):
|
|
7
|
-
description = DESCRIPTION_FIELD
|
|
8
|
-
ref = REF_FIELD
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
from marshmallow import validate
|
|
3
|
-
|
|
4
|
-
from ..base import BaseSchema
|
|
5
|
-
from ..constants import OPENAPI_VERSION
|
|
6
|
-
from ..fields import ENDPOINT_FIELD
|
|
7
|
-
from .components_object_schema import ComponentsObjectSchema
|
|
8
|
-
from .info_schema import InfoSchema
|
|
9
|
-
from .path_item_object_schema import PathItemObjectSchema
|
|
10
|
-
from .server_schema import ServerSchema
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class RootSchema(BaseSchema):
|
|
14
|
-
openapi = fields.String(required=True, validate=validate.Equal(OPENAPI_VERSION))
|
|
15
|
-
info = fields.Nested(InfoSchema, required=True)
|
|
16
|
-
paths = fields.Dict(
|
|
17
|
-
required=True,
|
|
18
|
-
keys=ENDPOINT_FIELD,
|
|
19
|
-
values=fields.Nested(PathItemObjectSchema, required=True),
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
jsonSchemaDialect = fields.URL()
|
|
23
|
-
|
|
24
|
-
servers = fields.Nested(ServerSchema, many=True)
|
|
25
|
-
components = fields.Nested(ComponentsObjectSchema)
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
from marshmallow import fields
|
|
4
|
-
from marshmallow import validate
|
|
5
|
-
from marshmallow import validates
|
|
6
|
-
from marshmallow import validates_schema
|
|
7
|
-
from marshmallow import ValidationError
|
|
8
|
-
|
|
9
|
-
from ..base import BaseSchema
|
|
10
|
-
from ..constants import FORMATS
|
|
11
|
-
from ..constants import TYPES
|
|
12
|
-
from ..fields import DESCRIPTION_FIELD
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class FormatBinarySchema(BaseSchema):
|
|
16
|
-
default = fields.String()
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class FormatEmailSchema(BaseSchema):
|
|
20
|
-
default = fields.Email()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class FormatDateSchema(BaseSchema):
|
|
24
|
-
default = fields.Date()
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class FormatDateTimeSchema(BaseSchema):
|
|
28
|
-
default = fields.AwareDateTime(format='iso', default_timezone=None)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class FormatIPv4Schema(BaseSchema):
|
|
32
|
-
default = fields.IPv4()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class FormatIPv6Schema(BaseSchema):
|
|
36
|
-
default = fields.IPv6()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class FormatTimeSchema(BaseSchema):
|
|
40
|
-
default = fields.Time(format='iso')
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class FormatURISchema(BaseSchema):
|
|
44
|
-
default = fields.URL()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class FormatUUIDSchema(BaseSchema):
|
|
48
|
-
default = fields.UUID()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
format_schemas = {
|
|
52
|
-
'binary': FormatBinarySchema,
|
|
53
|
-
'date': FormatDateSchema,
|
|
54
|
-
'date-time': FormatDateTimeSchema,
|
|
55
|
-
'email': FormatEmailSchema,
|
|
56
|
-
'ipv4': FormatIPv4Schema,
|
|
57
|
-
'ipv6': FormatIPv6Schema,
|
|
58
|
-
'time': FormatTimeSchema,
|
|
59
|
-
'uri': FormatURISchema,
|
|
60
|
-
'uuid': FormatUUIDSchema,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class StringFieldSchema(BaseSchema):
|
|
65
|
-
format = fields.String(validate=validate.OneOf(FORMATS))
|
|
66
|
-
minLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
67
|
-
maxLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
68
|
-
pattern = fields.String()
|
|
69
|
-
|
|
70
|
-
@validates('pattern')
|
|
71
|
-
def validate_pattern(self, value: str, data_key: str) -> None:
|
|
72
|
-
try:
|
|
73
|
-
re.compile(value)
|
|
74
|
-
except re.PatternError as e:
|
|
75
|
-
raise ValidationError(f"Pattern <{value}> is error <{repr(e)}>.")
|
|
76
|
-
|
|
77
|
-
@validates_schema
|
|
78
|
-
def validate_default(self, data, **kwargs):
|
|
79
|
-
if 'default' in data and 'pattern' in data:
|
|
80
|
-
result = re.match(data['pattern'], data['default'])
|
|
81
|
-
if result is None:
|
|
82
|
-
raise ValidationError(f'<{data["default"]}> does not match <{data["pattern"]}>')
|
|
83
|
-
|
|
84
|
-
@validates_schema
|
|
85
|
-
def validate_default_via_format(self, data, **kwargs):
|
|
86
|
-
if 'default' in data and 'format' in data:
|
|
87
|
-
error = format_schemas[data['format']]().validate({'default': data['default']})
|
|
88
|
-
if error:
|
|
89
|
-
raise ValidationError(str(error))
|
|
90
|
-
|
|
91
|
-
@validates_schema
|
|
92
|
-
def validate_length(self, data, **kwargs):
|
|
93
|
-
if 'minLength' in data and 'maxLength' in data:
|
|
94
|
-
if data['minLength'] > data['maxLength']:
|
|
95
|
-
raise ValidationError(
|
|
96
|
-
f'<{data['minLength']}> cannot be greater than <{data['maxLength']}>'
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class SchemaObjectSchema(StringFieldSchema):
|
|
101
|
-
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
102
|
-
default = fields.String()
|
|
103
|
-
description = DESCRIPTION_FIELD
|
|
104
|
-
nullable = fields.Boolean()
|
|
105
|
-
required = fields.List(fields.String())
|
|
106
|
-
additionalProperties = fields.Boolean()
|
|
107
|
-
|
|
108
|
-
properties = fields.Dict(
|
|
109
|
-
keys=fields.String(required=True, validate=validate.Length(min=1)),
|
|
110
|
-
values=fields.Nested(lambda: SchemaObjectSchema()),
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
@validates_schema
|
|
114
|
-
def validate_required(self, data, **kwargs):
|
|
115
|
-
if 'required' in data:
|
|
116
|
-
for field_name in data['required']:
|
|
117
|
-
if field_name not in data['properties']:
|
|
118
|
-
raise ValidationError(
|
|
119
|
-
f'Required field <{field_name}> not in <data["properties"]>'
|
|
120
|
-
)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
from marshmallow import Schema
|
|
3
|
-
|
|
4
|
-
from src.schema_first.openapi import OpenAPI
|
|
5
|
-
from src.schema_first.specification import Specification
|
|
6
|
-
from tests.utils import get_schema_from_request
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_specification__required(fx_spec_required, fx_spec_as_file):
|
|
10
|
-
spec_file = fx_spec_as_file(fx_spec_required)
|
|
11
|
-
spec = Specification(spec_file)
|
|
12
|
-
|
|
13
|
-
assert isinstance(spec.openapi, OpenAPI)
|
|
14
|
-
assert spec.reassembly_spec is None
|
|
15
|
-
|
|
16
|
-
spec.load()
|
|
17
|
-
|
|
18
|
-
request_schema = get_schema_from_request(spec.reassembly_spec, '/endpoint', '200')
|
|
19
|
-
assert isinstance(request_schema().fields['message'], fields.String)
|
|
20
|
-
assert request_schema().load({'message': 'Valid string'})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_specification__full(fx_spec_full, fx_spec_as_file):
|
|
24
|
-
spec_file = fx_spec_as_file(fx_spec_full)
|
|
25
|
-
spec = Specification(spec_file)
|
|
26
|
-
|
|
27
|
-
assert isinstance(spec.openapi, OpenAPI)
|
|
28
|
-
assert spec.reassembly_spec is None
|
|
29
|
-
|
|
30
|
-
spec.load()
|
|
31
|
-
|
|
32
|
-
request_schema = get_schema_from_request(spec.reassembly_spec, '/endpoint', '200')
|
|
33
|
-
assert isinstance(request_schema(), Schema)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|