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.
Files changed (51) hide show
  1. {schema_first-0.4.0/src/Schema_First.egg-info → schema_first-0.11.0}/PKG-INFO +11 -11
  2. {schema_first-0.4.0 → schema_first-0.11.0}/pyproject.toml +11 -11
  3. {schema_first-0.4.0 → schema_first-0.11.0/src/Schema_First.egg-info}/PKG-INFO +11 -11
  4. {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/SOURCES.txt +10 -5
  5. schema_first-0.11.0/src/Schema_First.egg-info/requires.txt +12 -0
  6. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/__init__.py +2 -2
  7. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/base.py +8 -0
  8. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/constants.py +4 -1
  9. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/fields.py +2 -2
  10. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +2 -2
  11. 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
  12. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/example_object_schema.py +21 -0
  13. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/external_docs_object_schema.py +13 -0
  14. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/info_object_schema.py +18 -0
  15. 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
  16. {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
  17. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/openapi_object_schema.py +57 -0
  18. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +21 -0
  19. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/parameter_object_schema.py +17 -0
  20. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +21 -0
  21. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +7 -0
  22. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +2 -3
  23. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +242 -0
  24. 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
  25. schema_first-0.11.0/src/schema_first/openapi/schemas/v3_1_1/tag_object_schema.py +15 -0
  26. schema_first-0.11.0/src/schema_first/openapi/schemas/validators.py +29 -0
  27. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/specification/__init__.py +46 -8
  28. schema_first-0.11.0/tests/test_specification.py +43 -0
  29. {schema_first-0.4.0 → schema_first-0.11.0}/tests/test_validator.py +2 -0
  30. schema_first-0.4.0/src/Schema_First.egg-info/requires.txt +0 -12
  31. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -21
  32. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -12
  33. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +0 -9
  34. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -8
  35. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -25
  36. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +0 -120
  37. schema_first-0.4.0/tests/test_specification.py +0 -33
  38. {schema_first-0.4.0 → schema_first-0.11.0}/LICENSE +0 -0
  39. {schema_first-0.4.0 → schema_first-0.11.0}/README.md +0 -0
  40. {schema_first-0.4.0 → schema_first-0.11.0}/setup.cfg +0 -0
  41. {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
  42. {schema_first-0.4.0 → schema_first-0.11.0}/src/Schema_First.egg-info/top_level.txt +0 -0
  43. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/__init__.py +0 -0
  44. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/exceptions.py +0 -0
  45. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/__init__.py +0 -0
  46. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/exc.py +0 -0
  47. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/loaders/yaml_loader.py +0 -0
  48. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/exc.py +0 -0
  49. {schema_first-0.4.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
  50. {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
  51. {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.4.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.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
- Requires-Dist: PyYAML>=6.0.2
37
+ Requires-Dist: PyYAML>=6.0.3
38
38
  Provides-Extra: dev
39
- Requires-Dist: bandit==1.8.6; extra == "dev"
40
- Requires-Dist: build==1.3.0; extra == "dev"
41
- Requires-Dist: openapi-spec-validator>=0.5.0; extra == "dev"
42
- Requires-Dist: pre-commit==4.2.0; extra == "dev"
43
- Requires-Dist: pytest==8.4.1; extra == "dev"
44
- Requires-Dist: pytest-cov==6.2.1; extra == "dev"
45
- Requires-Dist: python-dotenv==1.1.1; extra == "dev"
46
- Requires-Dist: twine==6.1.0; extra == "dev"
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.2'
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.14"
23
- version = "0.4.0"
22
+ requires-python = ">=3.13"
23
+ version = "0.11.0"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  dev = [
27
- "bandit==1.8.6",
28
- "build==1.3.0",
29
- 'openapi-spec-validator>=0.5.0',
30
- "pre-commit==4.2.0",
31
- "pytest==8.4.1",
32
- "pytest-cov==6.2.1",
33
- "python-dotenv==1.1.1",
34
- "twine==6.1.0"
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.4.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.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
- Requires-Dist: PyYAML>=6.0.2
37
+ Requires-Dist: PyYAML>=6.0.3
38
38
  Provides-Extra: dev
39
- Requires-Dist: bandit==1.8.6; extra == "dev"
40
- Requires-Dist: build==1.3.0; extra == "dev"
41
- Requires-Dist: openapi-spec-validator>=0.5.0; extra == "dev"
42
- Requires-Dist: pre-commit==4.2.0; extra == "dev"
43
- Requires-Dist: pytest==8.4.1; extra == "dev"
44
- Requires-Dist: pytest-cov==6.2.1; extra == "dev"
45
- Requires-Dist: python-dotenv==1.1.1; extra == "dev"
46
- Requires-Dist: twine==6.1.0; extra == "dev"
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/contact_schema.py
22
- src/schema_first/openapi/schemas/v3_1_1/info_schema.py
23
- src/schema_first/openapi/schemas/v3_1_1/license_schema.py
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/server_schema.py
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
@@ -0,0 +1,12 @@
1
+ marshmallow>=4.0.0
2
+ PyYAML>=6.0.3
3
+
4
+ [dev]
5
+ bandit>=1.9.3
6
+ build>=1.4.0
7
+ openapi-spec-validator>=0.7.1
8
+ pre-commit>=4.5.1
9
+ pytest>=9.0.2
10
+ pytest-cov>=7.0.0
11
+ python-dotenv>=1.2.1
12
+ twine==6.2.0
@@ -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.root_schema import RootSchema
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 RootSchema().load(self.raw_spec)
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
- OPENAPI_VERSION = '3.1.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 ResponsesObjectSchema
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(ResponsesObjectSchema, required=True)
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)
@@ -3,7 +3,7 @@ from marshmallow import fields
3
3
  from ..base import BaseSchema
4
4
 
5
5
 
6
- class ContactSchema(BaseSchema):
6
+ class ContactObjectSchema(BaseSchema):
7
7
  name = fields.String()
8
8
  url = fields.URL()
9
9
  email = fields.Email()
@@ -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)
@@ -5,7 +5,7 @@ from marshmallow import ValidationError
5
5
  from ..base import BaseSchema
6
6
 
7
7
 
8
- class LicenseSchema(BaseSchema):
8
+ class LicenseObjectSchema(BaseSchema):
9
9
  name = fields.String(required=True)
10
10
 
11
11
  identifier = fields.String()
@@ -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)
@@ -0,0 +1,7 @@
1
+ from ..base import BaseSchema
2
+ from ..base import DocStringFields
3
+ from ..fields import REF_FIELD
4
+
5
+
6
+ class ReferenceObjectSchema(DocStringFields, BaseSchema):
7
+ ref = REF_FIELD
@@ -1,11 +1,10 @@
1
1
  from marshmallow import fields
2
2
 
3
3
  from ..base import BaseSchema
4
- from ..fields import DESCRIPTION_FIELD
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 ResponsesObjectSchema(BaseSchema):
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 ServerSchema(BaseSchema):
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 = schema.get('enum')
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 _convert_field_any_type(self, field_schema: dict, required: bool = False) -> dict:
75
- if field_schema['type'] == 'string':
76
- converted_field_schema = self._convert_string_field(field_schema, required=required)
77
- else:
78
- raise NotImplementedError(field_schema)
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
- if k == 'schema':
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,12 +0,0 @@
1
- marshmallow>=4.0.0
2
- PyYAML>=6.0.2
3
-
4
- [dev]
5
- bandit==1.8.6
6
- build==1.3.0
7
- openapi-spec-validator>=0.5.0
8
- pre-commit==4.2.0
9
- pytest==8.4.1
10
- pytest-cov==6.2.1
11
- python-dotenv==1.1.1
12
- twine==6.1.0
@@ -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,9 +0,0 @@
1
- from marshmallow import fields
2
-
3
- from ..base import BaseSchema
4
- from .operation_object_schema import OperationObjectSchema
5
-
6
-
7
- class PathItemObjectSchema(BaseSchema):
8
- get = fields.Nested(OperationObjectSchema)
9
- post = fields.Nested(OperationObjectSchema)
@@ -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