Skip to content

Commit a5c07d8

Browse files
authored
Merge pull request #25 from bogdandm/swagger-json-test
Bug fixes
2 parents 7aac049 + 6dda51f commit a5c07d8

File tree

9 files changed

+3412
-24
lines changed

9 files changed

+3412
-24
lines changed

Diff for: README.md

+141
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ json2python-models is a [Python](https://www.python.org/) tool that can generate
4040

4141
## Example
4242

43+
### F1 Season Results
44+
45+
<details><summary>Show (long code)</summary>
46+
<p>
47+
4348
```
4449
driver_standings.json
4550
[
@@ -121,6 +126,142 @@ class DriverStandings:
121126
driver_standings: List['DriverStanding'] = attr.ib()
122127
```
123128

129+
</p>
130+
</details>
131+
132+
### Swagger
133+
134+
<details><summary>Show (long code)</summary>
135+
<p>
136+
137+
`swagger.json` from any online API (I tested file generated by drf-yasg and another one for Spotify API)
138+
139+
It requires a lit bit of tweaking:
140+
* Some fields store routes/models specs as dicts
141+
* There is a lot of optinal fields so we reduce merging threshold
142+
143+
```
144+
json_to_models -s flat -f dataclasses -m Swagger testing_tools/swagger.json
145+
--dict-keys-fields securityDefinitions paths responses definitions properties
146+
--merge percent_50 number
147+
```
148+
149+
```python
150+
from dataclasses import dataclass, field
151+
from json_to_models.dynamic_typing import FloatString
152+
from typing import Any, Dict, List, Optional, Union
153+
154+
155+
@dataclass
156+
class Swagger:
157+
swagger: FloatString
158+
info: 'Info'
159+
host: str
160+
schemes: List[str]
161+
base_path: str
162+
consumes: List[str]
163+
produces: List[str]
164+
security_definitions: Dict[str, 'Parameter_SecurityDefinition']
165+
security: List['Security']
166+
paths: Dict[str, 'Path']
167+
definitions: Dict[str, 'Definition_Schema']
168+
169+
170+
@dataclass
171+
class Info:
172+
title: str
173+
description: str
174+
version: str
175+
176+
177+
@dataclass
178+
class Security:
179+
api_key: Optional[List[Any]] = field(default_factory=list)
180+
basic: Optional[List[Any]] = field(default_factory=list)
181+
182+
183+
@dataclass
184+
class Path:
185+
parameters: List['Parameter_SecurityDefinition']
186+
post: Optional['Delete_Get_Patch_Post_Put'] = None
187+
get: Optional['Delete_Get_Patch_Post_Put'] = None
188+
put: Optional['Delete_Get_Patch_Post_Put'] = None
189+
patch: Optional['Delete_Get_Patch_Post_Put'] = None
190+
delete: Optional['Delete_Get_Patch_Post_Put'] = None
191+
192+
193+
@dataclass
194+
class Property:
195+
type: str
196+
format: Optional[str] = None
197+
xnullable: Optional[bool] = None
198+
items: Optional['Item_Schema'] = None
199+
200+
201+
@dataclass
202+
class Property_2E:
203+
type: str
204+
title: Optional[str] = None
205+
read_only: Optional[bool] = None
206+
max_length: Optional[int] = None
207+
min_length: Optional[int] = None
208+
items: Optional['Item'] = None
209+
enum: Optional[List[str]] = field(default_factory=list)
210+
maximum: Optional[int] = None
211+
minimum: Optional[int] = None
212+
format: Optional[str] = None
213+
214+
215+
@dataclass
216+
class Item:
217+
ref: Optional[str] = None
218+
title: Optional[str] = None
219+
type: Optional[str] = None
220+
max_length: Optional[int] = None
221+
min_length: Optional[int] = None
222+
223+
224+
@dataclass
225+
class Parameter_SecurityDefinition:
226+
name: str
227+
in_: str
228+
required: Optional[bool] = None
229+
schema: Optional['Item_Schema'] = None
230+
type: Optional[str] = None
231+
description: Optional[str] = None
232+
233+
234+
@dataclass
235+
class Delete_Get_Patch_Post_Put:
236+
operation_id: str
237+
description: str
238+
parameters: List['Parameter_SecurityDefinition']
239+
responses: Dict[str, 'Response']
240+
tags: List[str]
241+
242+
243+
@dataclass
244+
class Item_Schema:
245+
ref: str
246+
247+
248+
@dataclass
249+
class Response:
250+
description: str
251+
schema: Optional[Union['Item_Schema', 'Definition_Schema']] = None
252+
253+
254+
@dataclass
255+
class Definition_Schema:
256+
ref: Optional[str] = None
257+
required: Optional[List[str]] = field(default_factory=list)
258+
type: Optional[str] = None
259+
properties: Optional[Dict[str, Union['Property_2E', 'Property']]] = field(default_factory=dict)
260+
```
261+
262+
</p>
263+
</details>
264+
124265
## Installation
125266

126267
| **Be ware**: this project supports only `python3.7` and higher. |

Diff for: json_to_models/generator.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -237,23 +237,24 @@ def _optimize_union(self, t: DUnion):
237237

238238
types = [self.optimize_type(t) for t in other_types]
239239

240-
if Unknown in types:
241-
types.remove(Unknown)
242-
243-
optional = False
244-
if Null in types:
245-
optional = True
246-
while Null in types:
247-
types.remove(Null)
248240

249241
if len(types) > 1:
242+
if Unknown in types:
243+
types.remove(Unknown)
244+
245+
optional = False
246+
if Null in types:
247+
optional = True
248+
while Null in types:
249+
types.remove(Null)
250+
250251
meta_type = DUnion(*types)
251252
if len(meta_type.types) == 1:
252253
meta_type = meta_type.types[0]
254+
255+
if optional:
256+
return DOptional(meta_type)
253257
else:
254258
meta_type = types[0]
255259

256-
if optional:
257-
return DOptional(meta_type)
258-
else:
259-
return meta_type
260+
return meta_type

Diff for: json_to_models/models/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def __init__(self, model: ModelMeta, post_init_converters=False, convert_unicode
7575
self.model = model
7676
self.post_init_converters = post_init_converters
7777
self.convert_unicode = convert_unicode
78-
self.model.name = self.convert_class_name(self.model.name)
78+
self.model.set_raw_name(self.convert_class_name(self.model.name), generated=self.model.is_name_generated)
7979

8080
@cached_method
8181
def convert_class_name(self, name):

Diff for: json_to_models/registry.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import defaultdict
22
from itertools import chain, combinations
3-
from typing import Dict, Iterable, List, Set, Tuple
3+
from typing import Dict, FrozenSet, Iterable, List, Set, Tuple
44

55
from ordered_set import OrderedSet
66

@@ -153,15 +153,22 @@ def merge_models(self, generator, strict=False) -> List[Tuple[ModelMeta, Set[Mod
153153
flag = True
154154
while flag:
155155
flag = False
156-
new_groups: OrderedSet[Set[ModelMeta]] = OrderedSet()
157-
for gr1, gr2 in combinations(groups, 2):
158-
if gr1 & gr2:
159-
old_len = len(new_groups)
160-
new_groups.add(frozenset(gr1 | gr2))
161-
added = old_len < len(new_groups)
162-
flag = flag or added
156+
new_groups: OrderedSet[FrozenSet[ModelMeta]] = OrderedSet()
157+
for gr1 in groups:
158+
in_set = False
159+
for gr2 in groups:
160+
if gr1 is gr2:
161+
continue
162+
if gr1 & gr2:
163+
in_set = True
164+
old_len = len(new_groups)
165+
new_groups.add(frozenset(gr1 | gr2))
166+
added = old_len < len(new_groups)
167+
flag = flag or added
168+
if not in_set:
169+
new_groups.add(gr1)
163170
if flag:
164-
groups = new_groups
171+
groups: OrderedSet[FrozenSet[ModelMeta]] = new_groups
165172

166173
replaces = []
167174
replaces_ids = set()
@@ -172,8 +179,7 @@ def merge_models(self, generator, strict=False) -> List[Tuple[ModelMeta, Set[Mod
172179
replaces.append((model_meta, group))
173180

174181
for model_meta in self.models:
175-
if model_meta.index not in replaces_ids:
176-
generator.optimize_type(model_meta)
182+
generator.optimize_type(model_meta)
177183
return replaces
178184

179185
def _merge(self, generator, *models: ModelMeta):

Diff for: test/test_registry/test_registry_merge_models.py

+52
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,58 @@
214214
],
215215
id="merge_models_with_optional_field"
216216
),
217+
pytest.param(
218+
[
219+
{
220+
"a" + str(i): int for i in range(20)
221+
},
222+
{
223+
**{
224+
"a" + str(i): int for i in range(20)
225+
},
226+
**{
227+
"b" + str(i): int for i in range(20)
228+
}
229+
},
230+
{
231+
"b" + str(i): int for i in range(20)
232+
},
233+
{
234+
"c" + str(i): int for i in range(20)
235+
},
236+
{
237+
**{
238+
"b" + str(i): int for i in range(20)
239+
},
240+
**{
241+
"c" + str(i): int for i in range(20)
242+
}
243+
},
244+
{
245+
"field1": int
246+
},
247+
{
248+
"field1": int
249+
}
250+
],
251+
[
252+
{
253+
**{
254+
"a" + str(i): DOptional(int) for i in range(20)
255+
},
256+
**{
257+
"b" + str(i): DOptional(int) for i in range(20)
258+
},
259+
**{
260+
"c" + str(i): DOptional(int) for i in range(20)
261+
}
262+
},
263+
{
264+
"field1": int
265+
}
266+
],
267+
id="multistage_merge"
268+
),
217269
]
218270

219271

Diff for: testing_tools/real_apis/spotify-swagger.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from pathlib import Path
2+
3+
import yaml
4+
5+
from json_to_models.dynamic_typing.string_serializable import StringSerializable, registry
6+
from json_to_models.generator import MetadataGenerator
7+
from json_to_models.models.attr import AttrsModelCodeGenerator
8+
from json_to_models.models.base import generate_code
9+
from json_to_models.models.structure import compose_models_flat
10+
from json_to_models.registry import ModelFieldsNumberMatch, ModelFieldsPercentMatch, ModelRegistry
11+
12+
13+
@registry.add()
14+
class SwaggerRef(StringSerializable, str):
15+
@classmethod
16+
def to_internal_value(cls, value: str) -> 'SwaggerRef':
17+
if not value.startswith("#/"):
18+
raise ValueError(f"invalid literal for SwaggerRef: '{value}'")
19+
return cls(value)
20+
21+
def to_representation(self) -> str:
22+
return str(self)
23+
24+
25+
def load_data() -> dict:
26+
with (Path(__file__) / ".." / ".." / "spotify-swagger.yaml").open() as f:
27+
data = yaml.load(f, Loader=yaml.SafeLoader)
28+
return data
29+
30+
31+
def main():
32+
data = load_data()
33+
del data["paths"]
34+
35+
gen = MetadataGenerator(
36+
dict_keys_regex=[],
37+
dict_keys_fields=["securityDefinitions", "paths", "responses", "definitions", "properties", "scopes"]
38+
)
39+
reg = ModelRegistry(ModelFieldsPercentMatch(.5), ModelFieldsNumberMatch(10))
40+
fields = gen.generate(data)
41+
reg.process_meta_data(fields, model_name="Swagger")
42+
reg.merge_models(generator=gen)
43+
reg.generate_names()
44+
45+
structure = compose_models_flat(reg.models_map)
46+
code = generate_code(structure, AttrsModelCodeGenerator)
47+
print(code)
48+
49+
50+
if __name__ == '__main__':
51+
main()

0 commit comments

Comments
 (0)