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.
Files changed (41) hide show
  1. {schema_first-0.3.0/src/Schema_First.egg-info → schema_first-0.5.0}/PKG-INFO +3 -3
  2. {schema_first-0.3.0 → schema_first-0.5.0}/pyproject.toml +3 -7
  3. {schema_first-0.3.0 → schema_first-0.5.0/src/Schema_First.egg-info}/PKG-INFO +3 -3
  4. {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/requires.txt +1 -1
  5. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/__init__.py +2 -1
  6. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +162 -0
  7. schema_first-0.5.0/src/schema_first/specification/__init__.py +139 -0
  8. schema_first-0.5.0/tests/test_specification.py +23 -0
  9. {schema_first-0.3.0 → schema_first-0.5.0}/tests/test_validator.py +8 -8
  10. schema_first-0.3.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +0 -17
  11. schema_first-0.3.0/src/schema_first/specification/__init__.py +0 -47
  12. schema_first-0.3.0/tests/test_specification.py +0 -33
  13. {schema_first-0.3.0 → schema_first-0.5.0}/LICENSE +0 -0
  14. {schema_first-0.3.0 → schema_first-0.5.0}/README.md +0 -0
  15. {schema_first-0.3.0 → schema_first-0.5.0}/setup.cfg +0 -0
  16. {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/SOURCES.txt +0 -0
  17. {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
  18. {schema_first-0.3.0 → schema_first-0.5.0}/src/Schema_First.egg-info/top_level.txt +0 -0
  19. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/__init__.py +0 -0
  20. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/exceptions.py +0 -0
  21. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/__init__.py +0 -0
  22. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/exc.py +0 -0
  23. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/loaders/yaml_loader.py +0 -0
  24. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/exc.py +0 -0
  25. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
  26. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/base.py +0 -0
  27. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/constants.py +0 -0
  28. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/fields.py +0 -0
  29. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +0 -0
  30. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/contact_schema.py +0 -0
  31. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -0
  32. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/license_schema.py +0 -0
  33. {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
  34. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -0
  35. {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
  36. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -0
  37. {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
  38. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +0 -0
  39. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -0
  40. {schema_first-0.3.0 → schema_first-0.5.0}/src/schema_first/openapi/schemas/v3_1_1/server_schema.py +0 -0
  41. {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.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.14
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.2.1; extra == "dev"
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.14"
23
- version = "0.3.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.2.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.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.14
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.2.1; extra == "dev"
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"
@@ -3,7 +3,7 @@ PyYAML>=6.0.2
3
3
 
4
4
  [dev]
5
5
  bandit==1.8.6
6
- build==1.2.1
6
+ build==1.3.0
7
7
  openapi-spec-validator>=0.5.0
8
8
  pre-commit==4.2.0
9
9
  pytest==8.4.1
@@ -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 test_validator_minimal(fx_spec_minimal, fx_spec_as_file):
8
- spec_file = fx_spec_as_file(fx_spec_minimal)
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 == fx_spec_minimal
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 test_validator__minimal__external_validator(fx_spec_minimal, fx_spec_as_file):
22
- spec_file = fx_spec_as_file(fx_spec_minimal, external_validator=True)
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(fx_spec_minimal, fx_spec_as_file):
34
- fx_spec_minimal['wrong_field_name'] = 'wrong'
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(fx_spec_minimal)
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