Skip to content

Commit

Permalink
Merge 5cf4a41 into e391ea3
Browse files Browse the repository at this point in the history
  • Loading branch information
Fatal1ty committed Mar 29, 2023
2 parents e391ea3 + 5cf4a41 commit 298cd28
Show file tree
Hide file tree
Showing 13 changed files with 2,351 additions and 4 deletions.
1 change: 1 addition & 0 deletions mashumaro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ class BaseConfig:
dialect: Optional[Type[Dialect]] = None
omit_none: Union[bool, Literal[Sentinel.MISSING]] = Sentinel.MISSING
orjson_options: Optional[int] = 0
json_schema: Dict[str, Any] = {}
8 changes: 6 additions & 2 deletions mashumaro/core/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
__all__ = [
"parse_timezone",
"ConfigValue",
"UTC_OFFSET_PATTERN",
]


UTC_OFFSET_PATTERN = r"^UTC(([+-][0-2][0-9]):([0-5][0-9]))?$"
UTC_OFFSET_RE = re.compile(UTC_OFFSET_PATTERN)


def parse_timezone(s: str) -> datetime.timezone:
regexp = re.compile(r"^UTC(([+-][0-2][0-9]):([0-5][0-9]))?$")
match = regexp.match(s)
match = UTC_OFFSET_RE.match(s)
if not match:
raise ValueError(
f"Time zone {s} must be either UTC " f"or in format UTC[+-]hh:mm"
Expand Down
13 changes: 11 additions & 2 deletions mashumaro/core/meta/code/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from dataclasses import _FIELDS, MISSING, Field, is_dataclass # type: ignore
from functools import lru_cache

import typing_extensions

from mashumaro.config import (
ADD_DIALECT_SUPPORT,
TO_DICT_ADD_BY_ALIAS_FLAG,
Expand Down Expand Up @@ -111,11 +113,13 @@ def annotations(self) -> typing.Dict[str, typing.Any]:
return self.namespace.get("__annotations__", {})

def __get_field_types(
self, recursive: bool = True
self, recursive: bool = True, include_extras: bool = False
) -> typing.Dict[str, typing.Any]:
fields = {}
try:
field_type_hints = typing.get_type_hints(self.cls)
field_type_hints = typing_extensions.get_type_hints(
self.cls, include_extras=include_extras
)
except NameError as e:
name = get_name_error_name(e)
raise UnresolvedTypeReferenceError(self.cls, name) from None
Expand Down Expand Up @@ -150,6 +154,11 @@ def get_field_resolved_type_params(
def field_types(self) -> typing.Dict[str, typing.Any]:
return self.__get_field_types()

def get_field_types(
self, include_extras: bool = False
) -> typing.Dict[str, typing.Any]:
return self.__get_field_types(include_extras=include_extras)

@property # type: ignore
@lru_cache()
def dataclass_fields(self) -> typing.Dict[str, Field]:
Expand Down
9 changes: 9 additions & 0 deletions mashumaro/jsonschema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .builder import JSONSchemaBuilder, build_json_schema
from .dialects import DRAFT_2020_12, OPEN_API_3_1

__all__ = [
"JSONSchemaBuilder",
"build_json_schema",
"DRAFT_2020_12",
"OPEN_API_3_1",
]
136 changes: 136 additions & 0 deletions mashumaro/jsonschema/annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from dataclasses import dataclass
from numbers import Number
from typing import Dict, Set

from mashumaro.jsonschema.models import JSONSchema


class Annotation:
pass


class Constraint(Annotation):
pass


class NumberConstraint(Constraint):
pass


@dataclass(unsafe_hash=True)
class Minimum(NumberConstraint):
value: Number


@dataclass(unsafe_hash=True)
class Maximum(NumberConstraint):
value: Number


@dataclass(unsafe_hash=True)
class ExclusiveMinimum(NumberConstraint):
value: Number


@dataclass(unsafe_hash=True)
class ExclusiveMaximum(NumberConstraint):
value: Number


@dataclass(unsafe_hash=True)
class MultipleOf(NumberConstraint):
value: Number


class StringConstraint(Constraint):
pass


@dataclass(unsafe_hash=True)
class MinLength(StringConstraint):
value: int


@dataclass(unsafe_hash=True)
class MaxLength(StringConstraint):
value: int


@dataclass(unsafe_hash=True)
class Pattern(StringConstraint):
value: str


class ArrayConstraint(Constraint):
pass


@dataclass(unsafe_hash=True)
class MinItems(ArrayConstraint):
value: int


@dataclass(unsafe_hash=True)
class MaxItems(ArrayConstraint):
value: int


@dataclass(unsafe_hash=True)
class UniqueItems(ArrayConstraint):
value: bool


@dataclass(unsafe_hash=True)
class Contains(ArrayConstraint):
value: JSONSchema


@dataclass(unsafe_hash=True)
class MinContains(ArrayConstraint):
value: int


@dataclass(unsafe_hash=True)
class MaxContains(ArrayConstraint):
value: int


class ObjectConstraint(Constraint):
pass


@dataclass(unsafe_hash=True)
class MaxProperties(ObjectConstraint):
value: int


@dataclass(unsafe_hash=True)
class MinProperties(ObjectConstraint):
value: int


@dataclass
class DependentRequired(ObjectConstraint):
value: Dict[str, Set[str]]


__all__ = [
"Annotation",
"MultipleOf",
"Maximum",
"ExclusiveMaximum",
"Minimum",
"ExclusiveMinimum",
"MaxLength",
"MinLength",
"Pattern",
"MaxItems",
"MinItems",
"UniqueItems",
"Contains",
"MaxContains",
"MinContains",
"MaxProperties",
"MinProperties",
"DependentRequired",
]
64 changes: 64 additions & 0 deletions mashumaro/jsonschema/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Type

from mashumaro.jsonschema.dialects import DRAFT_2020_12, JSONSchemaDialect
from mashumaro.jsonschema.models import Context, JSONSchema
from mashumaro.jsonschema.schema import Instance, get_schema

try:
from mashumaro.mixins.orjson import (
DataClassORJSONMixin as DataClassJSONMixin,
)
except ImportError: # pragma no cover
from mashumaro.mixins.json import DataClassJSONMixin # type: ignore


def build_json_schema(
instance_type: Type,
context: Optional[Context] = None,
with_definitions: bool = True,
all_refs: Optional[bool] = None,
) -> JSONSchema:
if context is None:
context = Context()
if all_refs is not None:
context.all_refs = all_refs
instance = Instance(instance_type)
schema = get_schema(instance, context)
if with_definitions and context.definitions:
schema.definitions = context.definitions
return schema


@dataclass
class JSONSchemaDefinitions(DataClassJSONMixin):
definitions: Dict[str, JSONSchema]

def __post_serialize__( # type: ignore
self, d: Dict[Any, Any]
) -> List[Dict[str, Any]]:
return d["definitions"]


class JSONSchemaBuilder:
def __init__(
self,
dialect: JSONSchemaDialect = DRAFT_2020_12,
all_refs: Optional[bool] = None,
):
if all_refs is None:
all_refs = dialect.all_refs
self.context = Context(dialect=dialect, all_refs=all_refs)

def build(self, instance_type: Type) -> JSONSchema:
return build_json_schema(
instance_type=instance_type,
context=self.context,
with_definitions=False,
)

def get_definitions(self) -> JSONSchemaDefinitions:
return JSONSchemaDefinitions(self.context.definitions)


__all__ = ["JSONSchemaBuilder"]
29 changes: 29 additions & 0 deletions mashumaro/jsonschema/dialects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass


@dataclass(frozen=True)
class JSONSchemaDialect:
uri: str
definitions_root_pointer: str
all_refs: bool


@dataclass(frozen=True)
class JSONSchemaDraft202012Dialect(JSONSchemaDialect):
uri: str = "https://json-schema.org/draft/2020-12/schema"
definitions_root_pointer: str = "#/defs"
all_refs: bool = False


@dataclass(frozen=True)
class OpenAPISchema31Dialect(JSONSchemaDialect):
uri: str = "https://spec.openapis.org/oas/3.1/dialect/base"
definitions_root_pointer: str = "#/components/schemas"
all_refs: bool = True


DRAFT_2020_12 = JSONSchemaDraft202012Dialect()
OPEN_API_3_1 = OpenAPISchema31Dialect()


__all__ = ["JSONSchemaDialect", "DRAFT_2020_12", "OPEN_API_3_1"]

0 comments on commit 298cd28

Please sign in to comment.