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
108 changes: 56 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,47 +83,46 @@ driver_standings.json
```

```
json2models -f attrs -l DriverStandings driver_standings.json
json2models -f pydantic -s flat -l DriverStandings - driver_standings.json
```

```python
import attr
from json_to_models.dynamic_typing import IntString, IsoDateString
r"""
generated by json2python-models v0.1.2 at Mon May 4 17:46:30 2020
command: /opt/projects/json2python-models/venv/bin/json2models -f pydantic -s flat -l DriverStandings - driver_standings.json
"""
from pydantic import BaseModel, Field
from typing import List


@attr.s
class DriverStandings:
@attr.s
class DriverStanding:
@attr.s
class Driver:
driver_id: str = attr.ib()
permanent_number: IntString = attr.ib(converter=IntString)
code: str = attr.ib()
url: str = attr.ib()
given_name: str = attr.ib()
family_name: str = attr.ib()
date_of_birth: IsoDateString = attr.ib(converter=IsoDateString)
nationality: str = attr.ib()

@attr.s
class Constructor:
constructor_id: str = attr.ib()
url: str = attr.ib()
name: str = attr.ib()
nationality: str = attr.ib()

position: IntString = attr.ib(converter=IntString)
position_text: IntString = attr.ib(converter=IntString)
points: IntString = attr.ib(converter=IntString)
wins: IntString = attr.ib(converter=IntString)
driver: 'Driver' = attr.ib()
constructors: List['Constructor'] = attr.ib()

season: IntString = attr.ib(converter=IntString)
round: IntString = attr.ib(converter=IntString)
driver_standings: List['DriverStanding'] = attr.ib()
from typing_extensions import Literal

class DriverStandings(BaseModel):
season: int
round_: int = Field(..., alias="round")
DriverStandings: List['DriverStanding']

class DriverStanding(BaseModel):
position: int
position_text: int = Field(..., alias="positionText")
points: int
wins: int
driver: 'Driver' = Field(..., alias="Driver")
constructors: List['Constructor'] = Field(..., alias="Constructors")

class Driver(BaseModel):
driver_id: str = Field(..., alias="driverId")
permanent_number: int = Field(..., alias="permanentNumber")
code: str
url: str
given_name: str = Field(..., alias="givenName")
family_name: str = Field(..., alias="familyName")
date_of_birth: str = Field(..., alias="dateOfBirth")
nationality: str

class Constructor(BaseModel):
constructor_id: str = Field(..., alias="constructorId")
url: str
name: str
nationality: Literal["Austrian", "German", "American", "British", "Italian", "French"]
```

</p>
Expand All @@ -139,14 +138,19 @@ class DriverStandings:
It requires a lit bit of tweaking:
* Some fields store routes/models specs as dicts
* There is a lot of optinal fields so we reduce merging threshold
* Disable string literals

```
json_to_models -s flat -f dataclasses -m Swagger testing_tools/swagger.json
--dict-keys-fields securityDefinitions paths responses definitions properties
--merge percent_50 number
json2models -s flat -f dataclasses -m Swagger testing_tools/swagger.json \
--dict-keys-fields securityDefinitions paths responses definitions properties \
--merge percent_50 number --max-strings-literals 0
```

```python
r"""
generated by json2python-models v0.1.2 at Mon May 4 18:08:09 2020
command: /opt/projects/json2python-models/json_to_models/__main__.py -s flat -f dataclasses -m Swagger testing_tools/swagger.json --max-strings-literals 0 --dict-keys-fields securityDefinitions paths responses definitions properties --merge percent_50 number
"""
from dataclasses import dataclass, field
from json_to_models.dynamic_typing import FloatString
from typing import Any, Dict, List, Optional, Union
Expand Down Expand Up @@ -192,15 +196,15 @@ class Path:

@dataclass
class Property:
type: str
format: Optional[str] = None
type_: str
format_: Optional[str] = None
xnullable: Optional[bool] = None
items: Optional['Item_Schema'] = None


@dataclass
class Property_2E:
type: str
type_: str
title: Optional[str] = None
read_only: Optional[bool] = None
max_length: Optional[int] = None
Expand All @@ -209,26 +213,26 @@ class Property_2E:
enum: Optional[List[str]] = field(default_factory=list)
maximum: Optional[int] = None
minimum: Optional[int] = None
format: Optional[str] = None
format_: Optional[str] = None


@dataclass
class Item:
ref: Optional[str] = None
title: Optional[str] = None
type: Optional[str] = None
type_: Optional[str] = None
ref: Optional[str] = None
max_length: Optional[int] = None
min_length: Optional[int] = None


@dataclass
class Parameter_SecurityDefinition:
name: str
in_: str
name: Optional[str] = None
in_: Optional[str] = None
required: Optional[bool] = None
schema: Optional['Item_Schema'] = None
type: Optional[str] = None
description: Optional[str] = None
type_: Optional[str] = None


@dataclass
Expand All @@ -253,10 +257,10 @@ class Response:

@dataclass
class Definition_Schema:
ref: Optional[str] = None
type_: str
required: Optional[List[str]] = field(default_factory=list)
type: Optional[str] = None
properties: Optional[Dict[str, Union['Property_2E', 'Property']]] = field(default_factory=dict)
properties: Optional[Dict[str, Union['Property', 'Property_2E']]] = field(default_factory=dict)
ref: Optional[str] = None
```

</p>
Expand Down
18 changes: 16 additions & 2 deletions json_to_models/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def __init__(self):
self.models_data: Dict[str, Iterable[dict]] = {} # -m/-l
self.enable_datetime: bool = False # --datetime
self.strings_converters: bool = False # --strings-converters
self.max_literals: int = -1 # --max-strings-literals
self.merge_policy: List[ModelCmp] = [] # --merge
self.structure_fn: STRUCTURE_FN_TYPE = None # -s
self.model_generator: Type[GenericModelCodeGenerator] = None # -f & --code-generator
Expand Down Expand Up @@ -83,6 +84,7 @@ def parse_args(self, args: List[str] = None):
self.enable_datetime = namespace.datetime
disable_unicode_conversion = namespace.disable_unicode_conversion
self.strings_converters = namespace.strings_converters
self.max_literals = namespace.max_strings_literals
merge_policy = [m.split("_") if "_" in m else m for m in namespace.merge]
structure = namespace.structure
framework = namespace.framework
Expand Down Expand Up @@ -204,8 +206,11 @@ def set_args(
m = importlib.import_module(module)
self.model_generator = getattr(m, cls)

self.model_generator_kwargs = {} if not self.strings_converters else {'post_init_converters': True}
self.model_generator_kwargs['convert_unicode'] = not disable_unicode_conversion
self.model_generator_kwargs = dict(
post_init_converters=self.strings_converters,
convert_unicode=not disable_unicode_conversion,
max_literals=self.max_literals
)
if code_generator_kwargs_raw:
for item in code_generator_kwargs_raw:
if item[0] == '"':
Expand Down Expand Up @@ -279,6 +284,15 @@ def _create_argparser(cls) -> argparse.ArgumentParser:
action="store_true",
help="Enable generation of string types converters (i.e. IsoDatetimeString or BooleanString).\n\n"
)
parser.add_argument(
"--max-strings-literals",
type=int,
default=GenericModelCodeGenerator.DEFAULT_MAX_LITERALS,
metavar='NUMBER',
help="Generate Literal['foo', 'bar'] when field have less than NUMBER string constants as values.\n"
f"Pass 0 to disable. By default NUMBER={GenericModelCodeGenerator.DEFAULT_MAX_LITERALS}"
f" (some generator classes could override it)\n\n"
)
parser.add_argument(
"--disable-unicode-conversion", "--no-unidecode",
action="store_true",
Expand Down
2 changes: 1 addition & 1 deletion json_to_models/dynamic_typing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from .base import (
BaseType, ImportPathList, MetaData, Null, Unknown, get_hash_string
)
from .complex import ComplexType, DDict, DList, DOptional, DTuple, DUnion, SingleType
from .complex import ComplexType, DDict, DList, DOptional, DTuple, DUnion, SingleType, StringLiteral
from .models_meta import AbsoluteModelRef, ModelMeta, ModelPtr
from .string_datetime import IsoDateString, IsoDatetimeString, IsoTimeString, register_datetime_classes
from .string_serializable import (
Expand Down
26 changes: 22 additions & 4 deletions json_to_models/dynamic_typing/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from inspect import isclass
from typing import Any, Generator, Iterable, List, Tuple, Union
from typing import Any, Dict, Generator, Iterable, List, Tuple, Type, Union

ImportPathList = List[Tuple[str, Union[Iterable[str], str, None]]]

Expand All @@ -21,14 +21,30 @@ def replace(self, t: Union['MetaData', List['MetaData']], **kwargs) -> 'BaseType
"""
raise NotImplementedError()

def to_typing_code(self) -> Tuple[ImportPathList, str]:
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
-> Tuple[ImportPathList, str]:
"""
Return typing code that represents this metadata and import path of classes that are used in this code

:param types_style: Hints for .to_typing_code() for different type wrappers
:return: ((module_name, (class_name, ...)), code)
"""
raise NotImplementedError()

@classmethod
def get_options_for_type(
cls,
t: Union['BaseType', Type['BaseType']],
types_style: Dict[Union['BaseType', Type['BaseType']], dict]
) -> dict:
t_cls = t if isclass(t) else type(t)
mro = t_cls.__mro__
for base in mro:
options = types_style.get(base, ...)
if options is not Ellipsis:
return options
return {}

def to_hash_string(self) -> str:
"""
Return unique string that can be used to generate hash of type instance.
Expand Down Expand Up @@ -71,7 +87,8 @@ def __iter__(self) -> Iterable['MetaData']:
def replace(self, t: 'MetaData', **kwargs) -> 'UnknownType':
return self

def to_typing_code(self) -> Tuple[ImportPathList, str]:
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
-> Tuple[ImportPathList, str]:
return ([('typing', 'Any')], 'Any')

def to_hash_string(self) -> str:
Expand All @@ -90,7 +107,8 @@ def __iter__(self) -> Iterable['MetaData']:
def replace(self, t: 'MetaData', **kwargs) -> 'NoneType':
return self

def to_typing_code(self) -> Tuple[ImportPathList, str]:
def to_typing_code(self, types_style: Dict[Union['BaseType', Type['BaseType']], dict]) \
-> Tuple[ImportPathList, str]:
return ([], 'None')

def to_hash_string(self) -> str:
Expand Down
Loading