Skip to content

Commit 88e196c

Browse files
bkalashnikovbogdandm
bkalashnikov
authored andcommitted
SQLModel support
1 parent 202fbf8 commit 88e196c

File tree

6 files changed

+285
-11
lines changed

6 files changed

+285
-11
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@
88
![Example](/etc/convert.png)
99

1010
json2python-models is a [Python](https://www.python.org/) tool that can generate Python models classes
11-
([pydantic](https://github.com/samuelcolvin/pydantic), dataclasses, [attrs](https://github.com/python-attrs/attrs))
11+
([pydantic](https://docs.pydantic.dev/), ([sqlmodel](https://sqlmodel.tiangolo.com/),
12+
dataclasses, [attrs](https://www.attrs.org/en/stable/))
1213
from JSON datasets.
1314

1415
## Features
1516

1617
* Full **`typing` module** support
17-
* **Types merging** - if some field contains data of different types this will be represent as `Union` type
18+
* **Types merging** - if some field contains data of different types this will be represented as `Union` type
1819
* Fields and models **names** generation (unicode support included)
1920
* Similar **models generalization**
20-
* Handling **recursive data** structures (i.e family tree)
21+
* Handling **recursive data** structures (i.e. family tree)
2122
* Detecting **string serializable types** (i.e. datetime or just stringify numbers)
2223
* Detecting fields containing string constants (`Literal['foo', 'bar']`)
2324
* Generation models as **list** (flat models structure) or **tree** (nested models)
@@ -157,7 +158,7 @@ class Constructor(BaseModel):
157158

158159
It requires a bit of tweaking:
159160
* Some fields store routes/models specs as dicts
160-
* There are a lot of optinal fields so we reduce merging threshold
161+
* There are a lot of optional fields, so we reduce merging threshold
161162
* Disable string literals
162163

163164
```
@@ -495,9 +496,10 @@ Arguments:
495496

496497
* `-f`, `--framework` - Model framework for which python code is generated.
497498
`base` (default) mean no framework so code will be generated without any decorators and additional meta-data.
498-
* **Format**: `-f {base, pydantic, attrs, dataclasses, custom}`
499+
* **Format**: `-f {base, pydantic, sqlmodel, attrs, dataclasses, custom}`
499500
* **Example**: `-f pydantic`
500501
* **Default**: `-f base`
502+
* **Warning**: SQLModel generator does not support Relationships and Foreign keys, they have to be added manually
501503

502504
* `-s`, `--structure` - Models composition style.
503505
* **Format**: `-s {flat, nested}`
@@ -546,7 +548,7 @@ Arguments:
546548
this dict will be marked as dict field but not nested model.
547549
* **Format**: `--dkr RegEx [RegEx ...]`
548550
* **Example**: `--dkr node_\d+ \d+_\d+_\d+`
549-
* **Note**: `^` and `$` (string borders) tokens will be added automatically but you
551+
* **Note**: `^` and `$` (string borders) tokens will be added automatically, but you
550552
have to escape other special characters manually.
551553
* **Optional**
552554

@@ -588,7 +590,7 @@ cd json2python-models
588590
python setup.py test -a '<pytest additional arguments>'
589591
```
590592

591-
Also I would recommend you to install `pytest-sugar` for pretty printing test results
593+
Also, I would recommend you to install `pytest-sugar` for pretty printing test results
592594

593595
### Test examples
594596

json_to_models/cli.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from pathlib import Path
1212
from typing import Any, Callable, Dict, Generator, Iterable, List, Tuple, Type, Union
1313

14+
from .models.sqlmodel import SqlModelCodeGenerator
15+
1416
try:
1517
import ruamel.yaml as yaml
1618
except ImportError:
@@ -55,6 +57,7 @@ class Cli:
5557
"dataclasses": convert_args(DataclassModelCodeGenerator, meta=bool_js_style,
5658
post_init_converters=bool_js_style),
5759
"pydantic": convert_args(PydanticModelCodeGenerator),
60+
"sqlmodel": convert_args(SqlModelCodeGenerator),
5861
}
5962

6063
def __init__(self):
@@ -122,7 +125,8 @@ def run(self):
122125
structure,
123126
self.model_generator,
124127
class_generator_kwargs=self.model_generator_kwargs,
125-
preamble=self.preamble)
128+
preamble=self.preamble
129+
)
126130
if self.output_file:
127131
with open(self.output_file, "w", encoding="utf-8") as f:
128132
f.write(output)

json_to_models/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ class ClassType(Enum):
1414
Dataclass = "dataclass"
1515
Attrs = "attrs"
1616
Pydantic = "pydantic"
17+
SqlModel = "sqlmodel"

json_to_models/models/pydantic.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
7070
"""
7171
imports, data = super().field_data(name, meta, optional)
7272
default: Optional[str] = None
73-
body_kwargs = {}
7473
if optional:
7574
meta: DOptional
7675
if isinstance(meta.type, DList):
@@ -80,8 +79,7 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
8079
else:
8180
default = "None"
8281

83-
if name != data["name"]:
84-
body_kwargs["alias"] = f'"{name}"'
82+
body_kwargs = self._get_field_kwargs(name, meta, optional, data)
8583
if body_kwargs:
8684
data["body"] = self.PYDANTIC_FIELD.render(
8785
default=default or '...',
@@ -90,3 +88,9 @@ def field_data(self, name: str, meta: MetaData, optional: bool) -> Tuple[ImportP
9088
elif default is not None:
9189
data["body"] = default
9290
return imports, data
91+
92+
def _get_field_kwargs(self, name: str, meta: MetaData, optional: bool, data: dict):
93+
body_kwargs = {}
94+
if name != data["name"]:
95+
body_kwargs["alias"] = f'"{name}"'
96+
return body_kwargs

json_to_models/models/sqlmodel.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import List, Tuple
2+
3+
from json_to_models.dynamic_typing import ImportPathList, MetaData
4+
from json_to_models.models.base import GenericModelCodeGenerator
5+
from json_to_models.models.pydantic import PydanticModelCodeGenerator
6+
7+
8+
class SqlModelCodeGenerator(PydanticModelCodeGenerator):
9+
def generate(self, nested_classes: List[str] = None, extra: str = "", **kwargs) \
10+
-> Tuple[ImportPathList, str]:
11+
imports, body = GenericModelCodeGenerator.generate(
12+
self,
13+
bases='SQLModel, table=True',
14+
nested_classes=nested_classes,
15+
extra=extra
16+
)
17+
imports.append(('sqlmodel', ['SQLModel', 'Field']))
18+
body = """
19+
# Warn! This generated code does not respect SQLModel Relationship and foreign_key, please add them manually.
20+
""".strip() + '\n' + body
21+
return imports, body
22+
23+
def convert_field_name(self, name):
24+
if name in ('id', 'pk'):
25+
return name
26+
return super().convert_field_name(name)
27+
28+
def _get_field_kwargs(self, name: str, meta: MetaData, optional: bool, data: dict):
29+
kwargs = super()._get_field_kwargs(name, meta, optional, data)
30+
# Detect primary key
31+
if data['name'] in ('id', 'pk') and meta is int:
32+
kwargs['primary_key'] = True
33+
return kwargs
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
from typing import Dict, List
2+
3+
import pytest
4+
5+
from json_to_models.dynamic_typing import (
6+
DDict,
7+
DList,
8+
DOptional,
9+
DUnion,
10+
FloatString,
11+
IntString,
12+
ModelMeta,
13+
compile_imports,
14+
)
15+
from json_to_models.models.base import generate_code
16+
from json_to_models.models.sqlmodel import SqlModelCodeGenerator
17+
from json_to_models.models.structure import sort_fields
18+
from test.test_code_generation.test_models_code_generator import model_factory, trim
19+
20+
# Data structure:
21+
# pytest.param id -> {
22+
# "model" -> (model_name, model_metadata),
23+
# test_name -> expected, ...
24+
# }
25+
test_data = {
26+
"base": {
27+
"model": ("Test", {
28+
"foo": int,
29+
"Bar": int,
30+
"baz": float
31+
}),
32+
"fields_data": {
33+
"foo": {
34+
"name": "foo",
35+
"type": "int"
36+
},
37+
"Bar": {
38+
"name": "bar",
39+
"type": "int",
40+
"body": 'Field(..., alias="Bar")'
41+
},
42+
"baz": {
43+
"name": "baz",
44+
"type": "float"
45+
}
46+
},
47+
"fields": {
48+
"imports": "",
49+
"fields": [
50+
f"foo: int",
51+
f'bar: int = Field(..., alias="Bar")',
52+
f"baz: float",
53+
]
54+
},
55+
"generated": trim(f"""
56+
from sqlmodel import Field, SQLModel
57+
58+
59+
# Warn! This generated code does not respect SQLModel Relationship and foreign_key, please add them manually.
60+
class Test(SQLModel, table=True):
61+
foo: int
62+
bar: int = Field(..., alias="Bar")
63+
baz: float
64+
""")
65+
},
66+
"complex": {
67+
"model": ("Test", {
68+
"foo": int,
69+
"baz": DOptional(DList(DList(str))),
70+
"bar": DOptional(IntString),
71+
"qwerty": FloatString,
72+
"asdfg": DOptional(int),
73+
"dict": DDict(int),
74+
"not": bool,
75+
"1day": int,
76+
"день_недели": str,
77+
}),
78+
"fields_data": {
79+
"foo": {
80+
"name": "foo",
81+
"type": "int"
82+
},
83+
"baz": {
84+
"name": "baz",
85+
"type": "Optional[List[List[str]]]",
86+
"body": "[]"
87+
},
88+
"bar": {
89+
"name": "bar",
90+
"type": "Optional[int]",
91+
"body": "None"
92+
},
93+
"qwerty": {
94+
"name": "qwerty",
95+
"type": "float"
96+
},
97+
"asdfg": {
98+
"name": "asdfg",
99+
"type": "Optional[int]",
100+
"body": "None"
101+
},
102+
"dict": {
103+
"name": "dict_",
104+
"type": "Dict[str, int]",
105+
"body": 'Field(..., alias="dict")'
106+
},
107+
"not": {
108+
"name": "not_",
109+
"type": "bool",
110+
"body": 'Field(..., alias="not")'
111+
},
112+
"1day": {
113+
"name": "one_day",
114+
"type": "int",
115+
"body": 'Field(..., alias="1day")'
116+
},
117+
"день_недели": {
118+
"name": "den_nedeli",
119+
"type": "str",
120+
"body": 'Field(..., alias="день_недели")'
121+
}
122+
},
123+
"generated": trim(f"""
124+
from sqlmodel import Field, SQLModel
125+
from typing import Dict, List, Optional
126+
127+
128+
# Warn! This generated code does not respect SQLModel Relationship and foreign_key, please add them manually.
129+
class Test(SQLModel, table=True):
130+
foo: int
131+
qwerty: float
132+
dict_: Dict[str, int] = Field(..., alias="dict")
133+
not_: bool = Field(..., alias="not")
134+
one_day: int = Field(..., alias="1day")
135+
den_nedeli: str = Field(..., alias="день_недели")
136+
baz: Optional[List[List[str]]] = []
137+
bar: Optional[int] = None
138+
asdfg: Optional[int] = None
139+
""")
140+
},
141+
"converters": {
142+
"model": ("Test", {
143+
"a": int,
144+
"b": IntString,
145+
"c": DOptional(FloatString),
146+
"d": DList(DList(DList(IntString))),
147+
"e": DDict(IntString),
148+
"u": DUnion(DDict(IntString), DList(DList(IntString))),
149+
}),
150+
"generated": trim("""
151+
from sqlmodel import Field, SQLModel
152+
from typing import Dict, List, Optional, Union
153+
154+
155+
# Warn! This generated code does not respect SQLModel Relationship and foreign_key, please add them manually.
156+
class Test(SQLModel, table=True):
157+
a: int
158+
b: int
159+
d: List[List[List[int]]]
160+
e: Dict[str, int]
161+
u: Union[Dict[str, int], List[List[int]]]
162+
c: Optional[float] = None
163+
""")
164+
},
165+
"sql_models": {
166+
"model": ("Test", {
167+
"id": int,
168+
"name": str,
169+
"x": DList(int)
170+
}),
171+
"generated": trim("""
172+
from sqlmodel import Field, SQLModel
173+
from typing import List
174+
175+
176+
# Warn! This generated code does not respect SQLModel Relationship and foreign_key, please add them manually.
177+
class Test(SQLModel, table=True):
178+
id: int = Field(..., primary_key=True)
179+
name: str
180+
x: List[int]
181+
""")
182+
}
183+
}
184+
185+
test_data_unzip = {
186+
test: [
187+
pytest.param(
188+
model_factory(*data["model"]),
189+
data[test],
190+
id=id
191+
)
192+
for id, data in test_data.items()
193+
if test in data
194+
]
195+
for test in ("fields_data", "fields", "generated")
196+
}
197+
198+
199+
@pytest.mark.parametrize("value,expected", test_data_unzip["fields_data"])
200+
def test_fields_data_attr(value: ModelMeta, expected: Dict[str, dict]):
201+
gen = SqlModelCodeGenerator(value)
202+
required, optional = sort_fields(value)
203+
for is_optional, fields in enumerate((required, optional)):
204+
for field in fields:
205+
field_imports, data = gen.field_data(field, value.type[field], bool(is_optional))
206+
assert data == expected[field]
207+
208+
209+
@pytest.mark.parametrize("value,expected", test_data_unzip["fields"])
210+
def test_fields_attr(value: ModelMeta, expected: dict):
211+
expected_imports: str = expected["imports"]
212+
expected_fields: List[str] = expected["fields"]
213+
gen = SqlModelCodeGenerator(value)
214+
imports, fields = gen.fields
215+
imports = compile_imports(imports)
216+
assert imports == expected_imports
217+
assert fields == expected_fields
218+
219+
220+
@pytest.mark.parametrize("value,expected", test_data_unzip["generated"])
221+
def test_generated_attr(value: ModelMeta, expected: str):
222+
generated = generate_code(
223+
(
224+
[{"model": value, "nested": []}],
225+
{}
226+
),
227+
SqlModelCodeGenerator,
228+
class_generator_kwargs={}
229+
)
230+
assert generated.rstrip() == expected, generated

0 commit comments

Comments
 (0)