Schema-First 0.5.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 (50) hide show
  1. {schema_first-0.5.0/src/Schema_First.egg-info → schema_first-0.11.0}/PKG-INFO +10 -10
  2. {schema_first-0.5.0 → schema_first-0.11.0}/pyproject.toml +10 -10
  3. {schema_first-0.5.0 → schema_first-0.11.0/src/Schema_First.egg-info}/PKG-INFO +10 -10
  4. {schema_first-0.5.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.5.0 → schema_first-0.11.0}/src/schema_first/openapi/__init__.py +2 -2
  7. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/base.py +8 -0
  8. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/constants.py +4 -1
  9. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/fields.py +2 -2
  10. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +2 -2
  11. schema_first-0.5.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.5.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.5.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.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +2 -3
  23. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +103 -23
  24. schema_first-0.5.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.5.0 → schema_first-0.11.0}/src/schema_first/specification/__init__.py +20 -3
  28. schema_first-0.11.0/tests/test_specification.py +43 -0
  29. {schema_first-0.5.0 → schema_first-0.11.0}/tests/test_validator.py +2 -0
  30. schema_first-0.5.0/src/Schema_First.egg-info/requires.txt +0 -12
  31. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -21
  32. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -12
  33. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +0 -9
  34. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -8
  35. schema_first-0.5.0/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -25
  36. schema_first-0.5.0/tests/test_specification.py +0 -23
  37. {schema_first-0.5.0 → schema_first-0.11.0}/LICENSE +0 -0
  38. {schema_first-0.5.0 → schema_first-0.11.0}/README.md +0 -0
  39. {schema_first-0.5.0 → schema_first-0.11.0}/setup.cfg +0 -0
  40. {schema_first-0.5.0 → schema_first-0.11.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
  41. {schema_first-0.5.0 → schema_first-0.11.0}/src/Schema_First.egg-info/top_level.txt +0 -0
  42. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/__init__.py +0 -0
  43. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/exceptions.py +0 -0
  44. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/loaders/__init__.py +0 -0
  45. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/loaders/exc.py +0 -0
  46. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/loaders/yaml_loader.py +0 -0
  47. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/exc.py +0 -0
  48. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
  49. {schema_first-0.5.0 → schema_first-0.11.0}/src/schema_first/openapi/schemas/v3_1_1/request_body_object_schema.py +0 -0
  50. {schema_first-0.5.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.5.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
@@ -34,16 +34,16 @@ 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
22
  requires-python = ">=3.13"
23
- version = "0.5.0"
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.5.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
@@ -34,16 +34,16 @@ 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))
@@ -1,4 +1,5 @@
1
1
  from collections.abc import Mapping, Sequence
2
+ import math
2
3
  import re
3
4
  import typing
4
5
 
@@ -9,17 +10,26 @@ from marshmallow import validates
9
10
  from marshmallow import validates_schema
10
11
  from marshmallow import ValidationError
11
12
 
13
+ from schema_first.openapi.schemas.constants import FLOAT_FORMATS
14
+ from schema_first.openapi.schemas.constants import INT_FORMATS
15
+
12
16
  from ..base import BaseSchema
17
+ from ..base import DocStringFields
13
18
  from ..constants import FORMATS
14
19
  from ..constants import TYPES
15
- from ..fields import DESCRIPTION_FIELD
16
20
 
17
21
 
18
- class BaseSchemaField(BaseSchema):
22
+ class BaseSchemaField(DocStringFields, BaseSchema):
19
23
  type = fields.String(required=True, validate=validate.OneOf(TYPES))
20
- description = DESCRIPTION_FIELD
21
24
  nullable = fields.Boolean()
22
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
+
23
33
 
24
34
  class FormatBinarySchema(BaseSchema):
25
35
  default = fields.String()
@@ -57,17 +67,22 @@ class FormatUUIDSchema(BaseSchema):
57
67
  default = fields.UUID()
58
68
 
59
69
 
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
- }
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))
71
86
 
72
87
 
73
88
  class StringFieldSchema(BaseSchemaField):
@@ -91,13 +106,6 @@ class StringFieldSchema(BaseSchemaField):
91
106
  if result is None:
92
107
  raise ValidationError(f'<{data["default"]}> does not match <{data["pattern"]}>')
93
108
 
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
109
  @validates_schema
102
110
  def validate_length(self, data, **kwargs):
103
111
  if 'minLength' in data and 'maxLength' in data:
@@ -136,14 +144,86 @@ class BooleanFieldSchema(BaseSchemaField):
136
144
  raise ValidationError(f'<{data["default"]}> is not boolean.')
137
145
 
138
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
+
139
201
  field_schemas = {
140
202
  'boolean': BooleanFieldSchema,
203
+ 'integer': IntegerFieldSchema,
204
+ 'number': NumberFieldSchema,
141
205
  'object': ObjectFieldSchema,
142
206
  'string': StringFieldSchema,
143
207
  }
144
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
+
145
225
 
146
- class SchemaObjectSchema(BaseSchema):
226
+ class SchemaObjectSchema(BaseSchemaField):
147
227
  type = fields.String(required=True, validate=validate.OneOf(TYPES))
148
228
 
149
229
  def load(
@@ -159,4 +239,4 @@ class SchemaObjectSchema(BaseSchema):
159
239
  data, many=many, partial=partial, unknown=unknown
160
240
  )
161
241
  except KeyError:
162
- raise ValidationError(f'Data type <{data["type"]}> not supported.')
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
@@ -84,10 +83,26 @@ class Specification:
84
83
  initialized_schema.required = required
85
84
  return initialized_schema
86
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
+
87
100
  def _convert_field_any_type(self, field_schema: dict, required: bool = False):
88
101
  field_schema_converters = {
89
102
  'string': self._convert_string_field,
90
103
  'boolean': self._convert_boolean_field,
104
+ 'number': self._convert_number_field,
105
+ 'integer': self._convert_number_field,
91
106
  }
92
107
  try:
93
108
  converted_field_schema = field_schema_converters[field_schema['type']](
@@ -125,7 +140,9 @@ class Specification:
125
140
  def _reassembly_of_schemas(self, obj: Any) -> Any:
126
141
  if isinstance(obj, dict):
127
142
  for k, v in obj.items():
128
- 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):
129
146
  obj[k] = self._convert_from_openapi_to_marshmallow_schema(v)
130
147
  else:
131
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,23 +0,0 @@
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'})
File without changes
File without changes
File without changes