Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## Version 0.11.2

* Added support for multi-file specifications.

## Version 0.11.1

* Add `schema` field for parameter object.
Expand Down
525 changes: 504 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ authors = [
{name = "Konstantin Fadeev", email = "fadeev@legalact.pro"}
]
classifiers = [
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: LGPL-2.1 License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3"
]
dependencies = [
'marshmallow>=4.0.0',
'marshmallow>=4.3.0',
'PyYAML>=6.0.3'
]
description = "OpenAPI specification validator and converter to Marshmallow schemas."
license = {file = "LICENSE"}
name = "Schema-First"
readme = "README.md"
requires-python = ">=3.13"
version = "0.11.1"
version = "0.11.2"

[project.optional-dependencies]
dev = [
Expand Down
2 changes: 1 addition & 1 deletion src/schema_first/openapi/schemas/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from marshmallow import fields
from marshmallow import validate

ENDPOINT_FIELD = fields.String(required=True, validate=validate.Regexp(r'^[/][0-9a-z-{}/]*[^/]$'))
ENDPOINT_FIELD = fields.String(required=True, validate=validate.Regexp(r'^[/][0-9a-z-_{}/]*[^/]$'))
HTTP_CODE_FIELD = fields.String(required=True, validate=validate.Regexp(r'^[1-5]{1}\d{2}|default$'))
SUMMARY_FIELD = fields.String(validate=validate.Length(min=1, max=150))
DESCRIPTION_FIELD = fields.String(validate=validate.Length(min=1))
Expand Down
32 changes: 30 additions & 2 deletions src/schema_first/openapi/schemas/v3_1_1/schema_object_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,56 +33,71 @@ def validate_default_via_format(self, data, **kwargs):

class FormatBinarySchema(BaseSchema):
default = fields.String()
example = fields.String()


class FormatEmailSchema(BaseSchema):
default = fields.Email()
example = fields.Email()


class FormatDateSchema(BaseSchema):
default = fields.Date()
example = fields.Date()


class FormatDateTimeSchema(BaseSchema):
default = fields.AwareDateTime(format='iso', default_timezone=None)
example = fields.AwareDateTime(format='iso', default_timezone=None)


class FormatIPv4Schema(BaseSchema):
default = fields.IPv4()
example = fields.IPv4()


class FormatIPv6Schema(BaseSchema):
default = fields.IPv6()
example = fields.IPv6()


class FormatTimeSchema(BaseSchema):
default = fields.Time(format='iso')
example = fields.Time(format='iso')


class FormatURISchema(BaseSchema):
default = fields.URL()
example = fields.URL()


class FormatUUIDSchema(BaseSchema):
default = fields.UUID()
example = fields.UUID()


class FormatINT32Schema(BaseSchema):
default = fields.Integer(validate=validate.Range(min=-2_147_483_648, max=2_147_483_647))
example = fields.Integer(validate=validate.Range(min=-2_147_483_648, max=2_147_483_647))


class FormatINT64Schema(BaseSchema):
default = fields.Integer(
validate=validate.Range(min=-9_223_372_036_854_775_808, max=9_223_372_036_854_775_807)
)
example = fields.Integer(
validate=validate.Range(min=-9_223_372_036_854_775_808, max=9_223_372_036_854_775_807)
)


class FormatFloatSchema(BaseSchema):
default = fields.Float(validate=validate.Range(min=3.4e-38, max=3.4e38))
example = fields.Float(validate=validate.Range(min=3.4e-38, max=3.4e38))


class FormatDoubleSchema(BaseSchema):
default = fields.Float(validate=validate.Range(min=1.7e-308, max=1.7e308))
example = fields.Float(validate=validate.Range(min=1.7e-308, max=1.7e308))


class StringFieldSchema(BaseSchemaField):
Expand All @@ -91,6 +106,7 @@ class StringFieldSchema(BaseSchemaField):
maxLength = fields.Integer(validate=[validate.Range(min=0)])
pattern = fields.String()
default = fields.String()
example = fields.String()

@validates('pattern')
def validate_pattern(self, value: str, data_key: str) -> None:
Expand All @@ -100,11 +116,20 @@ def validate_pattern(self, value: str, data_key: str) -> None:
raise ValidationError(f"Pattern <{value}> is error <{repr(e)}>.")

@validates_schema
def validate_default(self, data, **kwargs):
def validate_default_and_example(self, data, **kwargs):
if 'default' in data and 'pattern' in data:
result = re.match(data['pattern'], data['default'])
if result is None:
raise ValidationError(f'<{data["default"]}> does not match <{data["pattern"]}>')
raise ValidationError(
f'<{data["default"]}> from default field does not match <{data["pattern"]}>'
)

if 'example' in data and 'pattern' in data:
result = re.match(data['pattern'], data['example'])
if result is None:
raise ValidationError(
f'<{data["example"]}> from example field does not match <{data["pattern"]}>'
)

@validates_schema
def validate_length(self, data, **kwargs):
Expand Down Expand Up @@ -136,6 +161,7 @@ def validate_required(self, data, **kwargs):

class BooleanFieldSchema(BaseSchemaField):
default = fields.Boolean(truthy=[True], falsy=[False])
example = fields.Boolean(truthy=[True], falsy=[False])

@validates_schema
def validate_default(self, data, **kwargs):
Expand All @@ -152,6 +178,7 @@ class NumberFieldSchema(BaseSchemaField):
exclusiveMaximum = fields.Float()
multipleOf = fields.Float(validate=[validate.Range(min=0, min_inclusive=False)])
default = fields.Float()
example = fields.Float()

@validates_schema
def validate_default(self, data, **kwargs):
Expand Down Expand Up @@ -196,6 +223,7 @@ class IntegerFieldSchema(NumberFieldSchema):
exclusiveMaximum = fields.Integer()
multipleOf = fields.Integer(validate=[validate.Range(min=0, min_inclusive=False)])
default = fields.Integer()
example = fields.Integer()


field_schemas = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ def test_string_field__pattern__default(fx_field_string__required):
with pytest.raises(ValidationError) as e:
SchemaObjectSchema().load(fx_field_string__required)

assert str(e.value) == "{'_schema': ['<Bad default.> does not match <^\\\\d*$>']}"
assert (
str(e.value)
== "{'_schema': ['<Bad default.> from default field does not match <^\\\\d*$>']}"
)


def test_string_field__format__default(fx_field_string__email):
Expand Down
26 changes: 26 additions & 0 deletions tests/test_specification.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from copy import deepcopy
from pathlib import Path

from marshmallow import fields
Expand Down Expand Up @@ -41,3 +42,28 @@ def test_specification_from_file(file_path):

spec = Specification(file_path)
spec.load()


def test_specification__multifile_required(fx_spec_as_file, fx_spec_required):
endpoint_spec = {'required-endpoint': deepcopy(fx_spec_required['paths']['/required-endpoint'])}
endpoint_file = fx_spec_as_file(endpoint_spec, file_name='required-endpoint.yaml')

root_file = fx_spec_required
root_file['paths']['/required-endpoint'] = {'$ref': f'{endpoint_file.name}#/required-endpoint'}
spec_file = fx_spec_as_file(root_file)

spec_as_dict, base_uri = read_from_filename(spec_file)
osv_validate(spec_as_dict, base_uri=base_uri)

spec = Specification(spec_file)
spec.load()


@pytest.mark.xfail(reason='Schema specification not fully realisation.')
def test_specification__multifile():
file_path = Path(tests_dir_abspath, '_contrib', 'specs', 'multifile', 'openapi', 'openapi.yaml')
spec_as_dict, base_uri = read_from_filename(file_path)
osv_validate(spec_as_dict, base_uri=base_uri)

spec = Specification(file_path)
spec.load()
Loading