Schema-First 0.3.0__tar.gz → 0.5.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.3.0/src/Schema_First.egg-info → schema_first-0.5.0}/PKG-INFO +3 -3
- {schema_first-0.3.0 → schema_first-0.5.0}/pyproject.toml +3 -7
- {schema_first-0.3.0 → schema_first-0.5.0/src/Schema_First.egg-info}/PKG-INFO +3 -3
- {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/requires.txt +1 -1
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/__init__.py +2 -1
- schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +162 -0
- schema_first-0.5.0/src/schema_first/specification/__init__.py +139 -0
- schema_first-0.5.0/tests/test_specification.py +23 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/tests/test_validator.py +8 -8
- schema_first-0.3.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +0 -17
- schema_first-0.3.0/src/schema_first/specification/__init__.py +0 -47
- schema_first-0.3.0/tests/test_specification.py +0 -33
- {schema_first-0.3.0 → schema_first-0.5.0}/LICENSE +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/README.md +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/setup.cfg +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/SOURCES.txt +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/top_level.txt +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/__init__.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/exceptions.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/__init__.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/exc.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/yaml_loader.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/exc.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/base.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/constants.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/fields.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/contact_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/license_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/media_type_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/request_body_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/server_schema.py +0 -0
- {schema_first-0.3.0 → schema_first-0.5.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.5.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,14 +30,14 @@ 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
37
|
Requires-Dist: PyYAML>=6.0.2
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: bandit==1.8.6; extra == "dev"
|
|
40
|
-
Requires-Dist: build==1.
|
|
40
|
+
Requires-Dist: build==1.3.0; extra == "dev"
|
|
41
41
|
Requires-Dist: openapi-spec-validator>=0.5.0; extra == "dev"
|
|
42
42
|
Requires-Dist: pre-commit==4.2.0; extra == "dev"
|
|
43
43
|
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
@@ -19,13 +19,13 @@ description = "OpenAPI specification validator and converter to Marshmallow sche
|
|
|
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.5.0"
|
|
24
24
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
dev = [
|
|
27
27
|
"bandit==1.8.6",
|
|
28
|
-
"build==1.
|
|
28
|
+
"build==1.3.0",
|
|
29
29
|
'openapi-spec-validator>=0.5.0',
|
|
30
30
|
"pre-commit==4.2.0",
|
|
31
31
|
"pytest==8.4.1",
|
|
@@ -53,10 +53,6 @@ target-version = ['py313']
|
|
|
53
53
|
profile = "google"
|
|
54
54
|
src_paths = ["src", "tests"]
|
|
55
55
|
|
|
56
|
-
[tool.pycln]
|
|
57
|
-
all = true
|
|
58
|
-
silence = true
|
|
59
|
-
|
|
60
56
|
[tool.setuptools.packages.find]
|
|
61
57
|
include = ["schema_first*"]
|
|
62
58
|
where = ["src"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Schema-First
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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,14 +30,14 @@ 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
37
|
Requires-Dist: PyYAML>=6.0.2
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: bandit==1.8.6; extra == "dev"
|
|
40
|
-
Requires-Dist: build==1.
|
|
40
|
+
Requires-Dist: build==1.3.0; extra == "dev"
|
|
41
41
|
Requires-Dist: openapi-spec-validator>=0.5.0; extra == "dev"
|
|
42
42
|
Requires-Dist: pre-commit==4.2.0; extra == "dev"
|
|
43
43
|
Requires-Dist: pytest==8.4.1; extra == "dev"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from pprint import pformat
|
|
2
3
|
|
|
3
4
|
from marshmallow import ValidationError
|
|
4
5
|
|
|
@@ -16,4 +17,4 @@ class OpenAPI:
|
|
|
16
17
|
try:
|
|
17
18
|
return RootSchema().load(self.raw_spec)
|
|
18
19
|
except ValidationError as e:
|
|
19
|
-
raise OpenAPIValidationError(e)
|
|
20
|
+
raise OpenAPIValidationError(f'\n{pformat(e.messages)}')
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from marshmallow import fields
|
|
6
|
+
from marshmallow import types
|
|
7
|
+
from marshmallow import validate
|
|
8
|
+
from marshmallow import validates
|
|
9
|
+
from marshmallow import validates_schema
|
|
10
|
+
from marshmallow import ValidationError
|
|
11
|
+
|
|
12
|
+
from ..base import BaseSchema
|
|
13
|
+
from ..constants import FORMATS
|
|
14
|
+
from ..constants import TYPES
|
|
15
|
+
from ..fields import DESCRIPTION_FIELD
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseSchemaField(BaseSchema):
|
|
19
|
+
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
20
|
+
description = DESCRIPTION_FIELD
|
|
21
|
+
nullable = fields.Boolean()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FormatBinarySchema(BaseSchema):
|
|
25
|
+
default = fields.String()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FormatEmailSchema(BaseSchema):
|
|
29
|
+
default = fields.Email()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FormatDateSchema(BaseSchema):
|
|
33
|
+
default = fields.Date()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FormatDateTimeSchema(BaseSchema):
|
|
37
|
+
default = fields.AwareDateTime(format='iso', default_timezone=None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FormatIPv4Schema(BaseSchema):
|
|
41
|
+
default = fields.IPv4()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FormatIPv6Schema(BaseSchema):
|
|
45
|
+
default = fields.IPv6()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FormatTimeSchema(BaseSchema):
|
|
49
|
+
default = fields.Time(format='iso')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FormatURISchema(BaseSchema):
|
|
53
|
+
default = fields.URL()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FormatUUIDSchema(BaseSchema):
|
|
57
|
+
default = fields.UUID()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
format_schemas = {
|
|
61
|
+
'binary': FormatBinarySchema,
|
|
62
|
+
'date': FormatDateSchema,
|
|
63
|
+
'date-time': FormatDateTimeSchema,
|
|
64
|
+
'email': FormatEmailSchema,
|
|
65
|
+
'ipv4': FormatIPv4Schema,
|
|
66
|
+
'ipv6': FormatIPv6Schema,
|
|
67
|
+
'time': FormatTimeSchema,
|
|
68
|
+
'uri': FormatURISchema,
|
|
69
|
+
'uuid': FormatUUIDSchema,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StringFieldSchema(BaseSchemaField):
|
|
74
|
+
format = fields.String(validate=validate.OneOf(FORMATS))
|
|
75
|
+
minLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
76
|
+
maxLength = fields.Integer(validate=[validate.Range(min=0)])
|
|
77
|
+
pattern = fields.String()
|
|
78
|
+
default = fields.String()
|
|
79
|
+
|
|
80
|
+
@validates('pattern')
|
|
81
|
+
def validate_pattern(self, value: str, data_key: str) -> None:
|
|
82
|
+
try:
|
|
83
|
+
re.compile(value)
|
|
84
|
+
except re.PatternError as e:
|
|
85
|
+
raise ValidationError(f"Pattern <{value}> is error <{repr(e)}>.")
|
|
86
|
+
|
|
87
|
+
@validates_schema
|
|
88
|
+
def validate_default(self, data, **kwargs):
|
|
89
|
+
if 'default' in data and 'pattern' in data:
|
|
90
|
+
result = re.match(data['pattern'], data['default'])
|
|
91
|
+
if result is None:
|
|
92
|
+
raise ValidationError(f'<{data["default"]}> does not match <{data["pattern"]}>')
|
|
93
|
+
|
|
94
|
+
@validates_schema
|
|
95
|
+
def validate_default_via_format(self, data, **kwargs):
|
|
96
|
+
if 'default' in data and 'format' in data:
|
|
97
|
+
error = format_schemas[data['format']]().validate({'default': data['default']})
|
|
98
|
+
if error:
|
|
99
|
+
raise ValidationError(str(error))
|
|
100
|
+
|
|
101
|
+
@validates_schema
|
|
102
|
+
def validate_length(self, data, **kwargs):
|
|
103
|
+
if 'minLength' in data and 'maxLength' in data:
|
|
104
|
+
if data['minLength'] > data['maxLength']:
|
|
105
|
+
raise ValidationError(
|
|
106
|
+
f'<{data["minLength"]}> cannot be greater than <{data["maxLength"]}>'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ObjectFieldSchema(BaseSchemaField):
|
|
111
|
+
required = fields.List(fields.String())
|
|
112
|
+
additionalProperties = fields.Boolean()
|
|
113
|
+
|
|
114
|
+
properties = fields.Dict(
|
|
115
|
+
keys=fields.String(required=True, validate=validate.Length(min=1)),
|
|
116
|
+
values=fields.Nested(lambda: SchemaObjectSchema()),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@validates_schema
|
|
120
|
+
def validate_required(self, data, **kwargs):
|
|
121
|
+
if 'required' in data:
|
|
122
|
+
for field_name in data['required']:
|
|
123
|
+
if field_name not in data['properties']:
|
|
124
|
+
raise ValidationError(
|
|
125
|
+
f'Required field <{field_name}> not in <data["properties"]>'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class BooleanFieldSchema(BaseSchemaField):
|
|
130
|
+
default = fields.Boolean(truthy=[True], falsy=[False])
|
|
131
|
+
|
|
132
|
+
@validates_schema
|
|
133
|
+
def validate_default(self, data, **kwargs):
|
|
134
|
+
if 'default' in data:
|
|
135
|
+
if not isinstance(data['default'], bool):
|
|
136
|
+
raise ValidationError(f'<{data["default"]}> is not boolean.')
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
field_schemas = {
|
|
140
|
+
'boolean': BooleanFieldSchema,
|
|
141
|
+
'object': ObjectFieldSchema,
|
|
142
|
+
'string': StringFieldSchema,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SchemaObjectSchema(BaseSchema):
|
|
147
|
+
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
148
|
+
|
|
149
|
+
def load(
|
|
150
|
+
self,
|
|
151
|
+
data: Mapping[str, typing.Any] | Sequence[Mapping[str, typing.Any]],
|
|
152
|
+
*,
|
|
153
|
+
many: bool | None = None,
|
|
154
|
+
partial: bool | types.StrSequenceOrSet | None = None,
|
|
155
|
+
unknown: types.UnknownOption | None = None,
|
|
156
|
+
):
|
|
157
|
+
try:
|
|
158
|
+
return field_schemas[data['type']]().load(
|
|
159
|
+
data, many=many, partial=partial, unknown=unknown
|
|
160
|
+
)
|
|
161
|
+
except KeyError:
|
|
162
|
+
raise ValidationError(f'Data type <{data["type"]}> not supported.')
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from marshmallow import fields
|
|
6
|
+
from marshmallow import INCLUDE
|
|
7
|
+
from marshmallow import RAISE
|
|
8
|
+
from marshmallow import Schema
|
|
9
|
+
from marshmallow import validate
|
|
10
|
+
|
|
11
|
+
from ..openapi import OpenAPI
|
|
12
|
+
|
|
13
|
+
FIELDS_VIA_TYPES = {
|
|
14
|
+
'boolean': fields.Boolean,
|
|
15
|
+
'number': fields.Float,
|
|
16
|
+
'string': fields.String,
|
|
17
|
+
'integer': fields.Integer,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
FIELDS_VIA_FORMATS = {
|
|
21
|
+
'uuid': fields.UUID,
|
|
22
|
+
'date-time': fields.AwareDateTime,
|
|
23
|
+
'date': fields.Date,
|
|
24
|
+
'time': fields.Time,
|
|
25
|
+
'email': fields.Email,
|
|
26
|
+
'ipv4': fields.IPv4,
|
|
27
|
+
'ipv6': fields.IPv6,
|
|
28
|
+
'uri': fields.Url,
|
|
29
|
+
'binary': fields.String,
|
|
30
|
+
'string': fields.String,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Specification:
|
|
35
|
+
def __init__(self, spec_file: Path | str):
|
|
36
|
+
self.openapi = OpenAPI(spec_file)
|
|
37
|
+
self.reassembly_spec = None
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _make_field_validators(schema: dict) -> list[validate.Validator]:
|
|
41
|
+
validators = []
|
|
42
|
+
|
|
43
|
+
if schema['type'] in ['string']:
|
|
44
|
+
validators.append(
|
|
45
|
+
validate.Length(min=schema.get('minLength'), max=schema.get('maxLength'))
|
|
46
|
+
)
|
|
47
|
+
if schema.get('pattern'):
|
|
48
|
+
validators.append(validate.Regexp(schema['pattern']))
|
|
49
|
+
|
|
50
|
+
if schema['type'] in ['integer', 'number']:
|
|
51
|
+
validators.append(validate.Range(min=schema.get('minimum'), max=schema.get('maximum')))
|
|
52
|
+
|
|
53
|
+
required_values = schema.get('enum')
|
|
54
|
+
if required_values:
|
|
55
|
+
validators.append(validate.OneOf(required_values))
|
|
56
|
+
|
|
57
|
+
return validators
|
|
58
|
+
|
|
59
|
+
def _convert_string_field(self, field_schema: dict, required: bool = False):
|
|
60
|
+
format_string = field_schema.get('format', 'string')
|
|
61
|
+
try:
|
|
62
|
+
schema = FIELDS_VIA_FORMATS[format_string]
|
|
63
|
+
except KeyError:
|
|
64
|
+
raise NotImplementedError(
|
|
65
|
+
f'Schema <{field_schema}> for format <{format_string}> not implemented.'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
initialized_schema = schema()
|
|
69
|
+
initialized_schema.validate = self._make_field_validators(field_schema)
|
|
70
|
+
initialized_schema.allow_none = field_schema.get('nullable', False)
|
|
71
|
+
initialized_schema.required = required
|
|
72
|
+
return initialized_schema
|
|
73
|
+
|
|
74
|
+
def _convert_boolean_field(self, field_schema: dict, required: bool = False):
|
|
75
|
+
try:
|
|
76
|
+
schema = FIELDS_VIA_TYPES[field_schema['type']]
|
|
77
|
+
except KeyError:
|
|
78
|
+
raise NotImplementedError(
|
|
79
|
+
f'Schema <{field_schema}> for type <{field_schema["type"]}> not implemented.'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
initialized_schema = schema()
|
|
83
|
+
initialized_schema.allow_none = field_schema.get('nullable', False)
|
|
84
|
+
initialized_schema.required = required
|
|
85
|
+
return initialized_schema
|
|
86
|
+
|
|
87
|
+
def _convert_field_any_type(self, field_schema: dict, required: bool = False):
|
|
88
|
+
field_schema_converters = {
|
|
89
|
+
'string': self._convert_string_field,
|
|
90
|
+
'boolean': self._convert_boolean_field,
|
|
91
|
+
}
|
|
92
|
+
try:
|
|
93
|
+
converted_field_schema = field_schema_converters[field_schema['type']](
|
|
94
|
+
field_schema, required=required
|
|
95
|
+
)
|
|
96
|
+
except KeyError:
|
|
97
|
+
raise NotImplementedError(
|
|
98
|
+
f'Schema <{field_schema}> for type <{field_schema["type"]}> not be converted.'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return converted_field_schema
|
|
102
|
+
|
|
103
|
+
def _convert_from_openapi_to_marshmallow_schema(self, open_api_schema: dict) -> type[Schema]:
|
|
104
|
+
marshmallow_schema = {}
|
|
105
|
+
for field_name, field_schema in open_api_schema['properties'].items():
|
|
106
|
+
required_fields = open_api_schema.get('required', [])
|
|
107
|
+
|
|
108
|
+
if field_name in required_fields:
|
|
109
|
+
is_required = True
|
|
110
|
+
else:
|
|
111
|
+
is_required = False
|
|
112
|
+
|
|
113
|
+
marshmallow_schema[field_name] = self._convert_field_any_type(
|
|
114
|
+
field_schema, required=is_required
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
additionalProperties = open_api_schema.get('additionalProperties', True)
|
|
118
|
+
if additionalProperties is False:
|
|
119
|
+
marshmallow_schema['unknown'] = RAISE
|
|
120
|
+
else:
|
|
121
|
+
marshmallow_schema['unknown'] = INCLUDE
|
|
122
|
+
|
|
123
|
+
return Schema.from_dict(marshmallow_schema)
|
|
124
|
+
|
|
125
|
+
def _reassembly_of_schemas(self, obj: Any) -> Any:
|
|
126
|
+
if isinstance(obj, dict):
|
|
127
|
+
for k, v in obj.items():
|
|
128
|
+
if k == 'schema':
|
|
129
|
+
obj[k] = self._convert_from_openapi_to_marshmallow_schema(v)
|
|
130
|
+
else:
|
|
131
|
+
self._reassembly_of_schemas(v)
|
|
132
|
+
|
|
133
|
+
def load(self) -> 'Specification':
|
|
134
|
+
self.openapi.load()
|
|
135
|
+
self.reassembly_spec = deepcopy(self.openapi.raw_spec)
|
|
136
|
+
|
|
137
|
+
self._reassembly_of_schemas(self.reassembly_spec)
|
|
138
|
+
|
|
139
|
+
return self
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from marshmallow import fields
|
|
2
|
+
from marshmallow import Schema
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from src.schema_first.openapi import OpenAPI
|
|
6
|
+
from src.schema_first.specification import Specification
|
|
7
|
+
from tests.utils import get_schema_from_request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize('fx', ['fx_spec_required', 'fx_spec_full'])
|
|
11
|
+
def test_specification(request, fx, fx_spec_as_file):
|
|
12
|
+
spec_file = fx_spec_as_file(request.getfixturevalue(fx))
|
|
13
|
+
spec = Specification(spec_file)
|
|
14
|
+
|
|
15
|
+
assert isinstance(spec.openapi, OpenAPI)
|
|
16
|
+
assert spec.reassembly_spec is None
|
|
17
|
+
|
|
18
|
+
spec.load()
|
|
19
|
+
|
|
20
|
+
request_schema = get_schema_from_request(spec.reassembly_spec, '/endpoint', '200')
|
|
21
|
+
assert isinstance(request_schema(), Schema)
|
|
22
|
+
assert isinstance(request_schema().fields['message'], fields.String)
|
|
23
|
+
assert request_schema().load({'message': 'Valid string'})
|
|
@@ -4,11 +4,11 @@ from src.schema_first.openapi import OpenAPI
|
|
|
4
4
|
from src.schema_first.openapi import OpenAPIValidationError
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
spec_file = fx_spec_as_file(
|
|
7
|
+
def test_validator_required(fx_spec_required, fx_spec_as_file):
|
|
8
|
+
spec_file = fx_spec_as_file(fx_spec_required)
|
|
9
9
|
open_api_spec = OpenAPI(spec_file)
|
|
10
10
|
open_api_spec.load()
|
|
11
|
-
assert open_api_spec.raw_spec ==
|
|
11
|
+
assert open_api_spec.raw_spec == fx_spec_required
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_validator__full(fx_spec_full, fx_spec_as_file):
|
|
@@ -18,8 +18,8 @@ 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
|
-
def
|
|
22
|
-
spec_file = fx_spec_as_file(
|
|
21
|
+
def test_validator__required__external_validator(fx_spec_required, fx_spec_as_file):
|
|
22
|
+
spec_file = fx_spec_as_file(fx_spec_required, external_validator=True)
|
|
23
23
|
open_api_spec = OpenAPI(spec_file)
|
|
24
24
|
open_api_spec.load()
|
|
25
25
|
|
|
@@ -30,10 +30,10 @@ def test_validator__full__external_validator(fx_spec_full, fx_spec_as_file):
|
|
|
30
30
|
open_api_spec.load()
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def test_validator__wrong_field_name(
|
|
34
|
-
|
|
33
|
+
def test_validator__wrong_field_name(fx_spec_required, fx_spec_as_file):
|
|
34
|
+
fx_spec_required['wrong_field_name'] = 'wrong'
|
|
35
35
|
|
|
36
|
-
spec_file = fx_spec_as_file(
|
|
36
|
+
spec_file = fx_spec_as_file(fx_spec_required)
|
|
37
37
|
|
|
38
38
|
open_api_spec = OpenAPI(spec_file)
|
|
39
39
|
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
from marshmallow import validate
|
|
3
|
-
|
|
4
|
-
from ..base import BaseSchema
|
|
5
|
-
from ..constants import FORMATS
|
|
6
|
-
from ..constants import TYPES
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SchemaObjectSchema(BaseSchema):
|
|
10
|
-
type = fields.String(required=True, validate=validate.OneOf(TYPES))
|
|
11
|
-
|
|
12
|
-
format = fields.String(validate=validate.OneOf(FORMATS))
|
|
13
|
-
pattern = fields.String()
|
|
14
|
-
|
|
15
|
-
properties = fields.Dict(
|
|
16
|
-
keys=fields.String(required=True), values=fields.Nested(lambda: SchemaObjectSchema())
|
|
17
|
-
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from copy import deepcopy
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from marshmallow import fields
|
|
6
|
-
from marshmallow import Schema
|
|
7
|
-
|
|
8
|
-
from ..openapi import OpenAPI
|
|
9
|
-
|
|
10
|
-
FIELDS_VIA_TYPES = {
|
|
11
|
-
'boolean': fields.Boolean,
|
|
12
|
-
'number': fields.Float,
|
|
13
|
-
'string': fields.String,
|
|
14
|
-
'integer': fields.Integer,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Specification:
|
|
19
|
-
def __init__(self, spec_file: Path | str):
|
|
20
|
-
self.openapi = OpenAPI(spec_file)
|
|
21
|
-
self.reassembly_spec = None
|
|
22
|
-
|
|
23
|
-
def _convert_from_openapi_to_marshmallow_schema(self, open_api_schema: dict) -> type[Schema]:
|
|
24
|
-
if open_api_schema['type'] == 'object':
|
|
25
|
-
marshmallow_schema = {}
|
|
26
|
-
for field_name, field in open_api_schema['properties'].items():
|
|
27
|
-
marshmallow_schema[field_name] = FIELDS_VIA_TYPES[field['type']]()
|
|
28
|
-
else:
|
|
29
|
-
raise NotImplementedError(open_api_schema)
|
|
30
|
-
|
|
31
|
-
return Schema.from_dict(marshmallow_schema)
|
|
32
|
-
|
|
33
|
-
def _reassembly_of_schemas(self, obj: Any) -> Any:
|
|
34
|
-
if isinstance(obj, dict):
|
|
35
|
-
for k, v in obj.items():
|
|
36
|
-
if k == 'schema':
|
|
37
|
-
obj[k] = self._convert_from_openapi_to_marshmallow_schema(v)
|
|
38
|
-
else:
|
|
39
|
-
self._reassembly_of_schemas(v)
|
|
40
|
-
|
|
41
|
-
def load(self) -> 'Specification':
|
|
42
|
-
self.openapi.load()
|
|
43
|
-
self.reassembly_spec = deepcopy(self.openapi.raw_spec)
|
|
44
|
-
|
|
45
|
-
self._reassembly_of_schemas(self.reassembly_spec)
|
|
46
|
-
|
|
47
|
-
return self
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from marshmallow import fields
|
|
2
|
-
|
|
3
|
-
from src.schema_first.openapi import OpenAPI
|
|
4
|
-
from src.schema_first.specification import Specification
|
|
5
|
-
from tests.utils import get_schema_from_request
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_specification__minimal(fx_spec_minimal, fx_spec_as_file):
|
|
9
|
-
spec_file = fx_spec_as_file(fx_spec_minimal)
|
|
10
|
-
spec = Specification(spec_file)
|
|
11
|
-
|
|
12
|
-
assert isinstance(spec.openapi, OpenAPI)
|
|
13
|
-
assert spec.reassembly_spec is None
|
|
14
|
-
|
|
15
|
-
spec.load()
|
|
16
|
-
|
|
17
|
-
request_schema = get_schema_from_request(spec.reassembly_spec, '/endpoint', '200')
|
|
18
|
-
assert isinstance(request_schema().fields['message'], fields.String)
|
|
19
|
-
assert request_schema().load({'message': 'Valid string'})
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_specification__full(fx_spec_full, fx_spec_as_file):
|
|
23
|
-
spec_file = fx_spec_as_file(fx_spec_full)
|
|
24
|
-
spec = Specification(spec_file)
|
|
25
|
-
|
|
26
|
-
assert isinstance(spec.openapi, OpenAPI)
|
|
27
|
-
assert spec.reassembly_spec is None
|
|
28
|
-
|
|
29
|
-
spec.load()
|
|
30
|
-
|
|
31
|
-
request_schema = get_schema_from_request(spec.reassembly_spec, '/endpoint', '200')
|
|
32
|
-
assert isinstance(request_schema().fields['message'], fields.String)
|
|
33
|
-
assert request_schema().load({'message': 'Valid string'})
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/contact_schema.py
RENAMED
|
File without changes
|
{schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/info_schema.py
RENAMED
|
File without changes
|
{schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/license_schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/root_schema.py
RENAMED
|
File without changes
|
{schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/server_schema.py
RENAMED
|
File without changes
|
|
File without changes
|