Schema-First 0.3.0__tar.gz → 0.4.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 (40) hide show
  1. {schema_first-0.3.0/src/Schema_First.egg-info → schema_first-0.4.0}/PKG-INFO +2 -2
  2. {schema_first-0.3.0 → schema_first-0.4.0}/pyproject.toml +2 -6
  3. {schema_first-0.3.0 → schema_first-0.4.0/src/Schema_First.egg-info}/PKG-INFO +2 -2
  4. {schema_first-0.3.0 → schema_first-0.4.0}/src/Schema_First.egg-info/requires.txt +1 -1
  5. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/__init__.py +2 -1
  6. schema_first-0.4.0/src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py +120 -0
  7. schema_first-0.4.0/src/schema_first/specification/__init__.py +118 -0
  8. {schema_first-0.3.0 → schema_first-0.4.0}/tests/test_specification.py +4 -4
  9. {schema_first-0.3.0 → schema_first-0.4.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 → schema_first-0.4.0}/LICENSE +0 -0
  13. {schema_first-0.3.0 → schema_first-0.4.0}/README.md +0 -0
  14. {schema_first-0.3.0 → schema_first-0.4.0}/setup.cfg +0 -0
  15. {schema_first-0.3.0 → schema_first-0.4.0}/src/Schema_First.egg-info/SOURCES.txt +0 -0
  16. {schema_first-0.3.0 → schema_first-0.4.0}/src/Schema_First.egg-info/dependency_links.txt +0 -0
  17. {schema_first-0.3.0 → schema_first-0.4.0}/src/Schema_First.egg-info/top_level.txt +0 -0
  18. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/__init__.py +0 -0
  19. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/exceptions.py +0 -0
  20. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/loaders/__init__.py +0 -0
  21. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/loaders/exc.py +0 -0
  22. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/loaders/yaml_loader.py +0 -0
  23. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/exc.py +0 -0
  24. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/__init__.py +0 -0
  25. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/base.py +0 -0
  26. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/constants.py +0 -0
  27. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/fields.py +0 -0
  28. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/components_object_schema.py +0 -0
  29. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/contact_schema.py +0 -0
  30. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/info_schema.py +0 -0
  31. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/license_schema.py +0 -0
  32. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/media_type_object_schema.py +0 -0
  33. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/operation_object_schema.py +0 -0
  34. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/path_item_object_schema.py +0 -0
  35. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/reference_object_schema.py +0 -0
  36. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/request_body_object_schema.py +0 -0
  37. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/responses_object_schema.py +0 -0
  38. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/root_schema.py +0 -0
  39. {schema_first-0.3.0 → schema_first-0.4.0}/src/schema_first/openapi/schemas/v3_1_1/server_schema.py +0 -0
  40. {schema_first-0.3.0 → schema_first-0.4.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.4.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
@@ -37,7 +37,7 @@ 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"
@@ -20,12 +20,12 @@ license = {file = "LICENSE"}
20
20
  name = "Schema-First"
21
21
  readme = "README.md"
22
22
  requires-python = ">=3.14"
23
- version = "0.3.0"
23
+ version = "0.4.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.4.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
@@ -37,7 +37,7 @@ 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,120 @@
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
+ )
@@ -0,0 +1,118 @@
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_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)
79
+
80
+ return converted_field_schema
81
+
82
+ def _convert_from_openapi_to_marshmallow_schema(self, open_api_schema: dict) -> type[Schema]:
83
+ marshmallow_schema = {}
84
+ for field_name, field_schema in open_api_schema['properties'].items():
85
+ required_fields = open_api_schema.get('required', [])
86
+
87
+ if field_name in required_fields:
88
+ is_required = True
89
+ else:
90
+ is_required = False
91
+
92
+ marshmallow_schema[field_name] = self._convert_field_any_type(
93
+ field_schema, required=is_required
94
+ )
95
+
96
+ additionalProperties = open_api_schema.get('additionalProperties', True)
97
+ if additionalProperties is False:
98
+ marshmallow_schema['unknown'] = RAISE
99
+ else:
100
+ marshmallow_schema['unknown'] = INCLUDE
101
+
102
+ return Schema.from_dict(marshmallow_schema)
103
+
104
+ def _reassembly_of_schemas(self, obj: Any) -> Any:
105
+ if isinstance(obj, dict):
106
+ for k, v in obj.items():
107
+ if k == 'schema':
108
+ obj[k] = self._convert_from_openapi_to_marshmallow_schema(v)
109
+ else:
110
+ self._reassembly_of_schemas(v)
111
+
112
+ def load(self) -> 'Specification':
113
+ self.openapi.load()
114
+ self.reassembly_spec = deepcopy(self.openapi.raw_spec)
115
+
116
+ self._reassembly_of_schemas(self.reassembly_spec)
117
+
118
+ return self
@@ -1,12 +1,13 @@
1
1
  from marshmallow import fields
2
+ from marshmallow import Schema
2
3
 
3
4
  from src.schema_first.openapi import OpenAPI
4
5
  from src.schema_first.specification import Specification
5
6
  from tests.utils import get_schema_from_request
6
7
 
7
8
 
8
- def test_specification__minimal(fx_spec_minimal, fx_spec_as_file):
9
- spec_file = fx_spec_as_file(fx_spec_minimal)
9
+ def test_specification__required(fx_spec_required, fx_spec_as_file):
10
+ spec_file = fx_spec_as_file(fx_spec_required)
10
11
  spec = Specification(spec_file)
11
12
 
12
13
  assert isinstance(spec.openapi, OpenAPI)
@@ -29,5 +30,4 @@ def test_specification__full(fx_spec_full, fx_spec_as_file):
29
30
  spec.load()
30
31
 
31
32
  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'})
33
+ assert isinstance(request_schema(), Schema)
@@ -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
File without changes
File without changes
File without changes