1
- from dataclasses import dataclass , replace
2
1
from itertools import chain
3
2
from typing import Any , ClassVar , Dict , Generic , List , Optional , Set , Tuple , TypeVar , Union
4
3
5
- from dateutil . parser import isoparse
4
+ import attr
6
5
7
6
from ... import schema as oai
8
7
from ... import utils
9
8
from ..errors import PropertyError , ValidationError
10
9
from ..reference import Reference
10
+ from .converter import convert , convert_chain
11
11
from .enum_property import EnumProperty
12
12
from .model_property import ModelProperty
13
13
from .property import Property
14
14
from .schemas import Schemas
15
15
16
16
17
- @dataclass
17
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
18
18
class StringProperty (Property ):
19
19
""" A property of type str """
20
20
21
21
max_length : Optional [int ] = None
22
22
pattern : Optional [str ] = None
23
-
24
23
_type_string : ClassVar [str ] = "str"
25
24
26
- def _validate_default (self , default : Any ) -> str :
27
- return f"{ utils .remove_string_escapes (default )!r} "
28
-
29
25
30
- @dataclass
26
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
31
27
class DateTimeProperty (Property ):
32
28
"""
33
29
A property of type datetime.datetime
@@ -48,15 +44,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
48
44
imports .update ({"import datetime" , "from typing import cast" , "from dateutil.parser import isoparse" })
49
45
return imports
50
46
51
- def _validate_default (self , default : Any ) -> str :
52
- try :
53
- isoparse (default )
54
- except (TypeError , ValueError ) as e :
55
- raise ValidationError from e
56
- return f"isoparse({ default !r} )"
57
47
58
-
59
- @dataclass
48
+ @attr .s (auto_attribs = True , frozen = True , slots = True )
60
49
class DateProperty (Property ):
61
50
""" A property of type datetime.date """
62
51
@@ -75,15 +64,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
75
64
imports .update ({"import datetime" , "from typing import cast" , "from dateutil.parser import isoparse" })
76
65
return imports
77
66
78
- def _validate_default (self , default : Any ) -> str :
79
- try :
80
- isoparse (default ).date ()
81
- except (TypeError , ValueError ) as e :
82
- raise ValidationError () from e
83
- return f"isoparse({ default !r} ).date()"
84
-
85
67
86
- @dataclass
68
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
87
69
class FileProperty (Property ):
88
70
""" A property used for uploading files """
89
71
@@ -103,49 +85,31 @@ def get_imports(self, *, prefix: str) -> Set[str]:
103
85
return imports
104
86
105
87
106
- @dataclass
88
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
107
89
class FloatProperty (Property ):
108
90
""" A property of type float """
109
91
110
- default : Optional [float ] = None
111
92
_type_string : ClassVar [str ] = "float"
112
93
113
- def _validate_default (self , default : Any ) -> float :
114
- try :
115
- return float (default )
116
- except (TypeError , ValueError ) as e :
117
- raise ValidationError () from e
118
-
119
94
120
- @dataclass
95
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
121
96
class IntProperty (Property ):
122
97
""" A property of type int """
123
98
124
- default : Optional [int ] = None
125
99
_type_string : ClassVar [str ] = "int"
126
100
127
- def _validate_default (self , default : Any ) -> int :
128
- try :
129
- return int (default )
130
- except (TypeError , ValueError ) as e :
131
- raise ValidationError () from e
132
-
133
101
134
- @dataclass
102
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
135
103
class BooleanProperty (Property ):
136
104
""" Property for bool """
137
105
138
106
_type_string : ClassVar [str ] = "bool"
139
107
140
- def _validate_default (self , default : Any ) -> bool :
141
- # no try/except needed as anything that comes from the initial load from json/yaml will be boolable
142
- return bool (default )
143
-
144
108
145
109
InnerProp = TypeVar ("InnerProp" , bound = Property )
146
110
147
111
148
- @dataclass
112
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
149
113
class ListProperty (Property , Generic [InnerProp ]):
150
114
""" A property representing a list (array) of other properties """
151
115
@@ -176,11 +140,8 @@ def get_imports(self, *, prefix: str) -> Set[str]:
176
140
imports .add ("from typing import List" )
177
141
return imports
178
142
179
- def _validate_default (self , default : Any ) -> None :
180
- return None
181
-
182
143
183
- @dataclass
144
+ @attr . s ( auto_attribs = True , frozen = True , slots = True )
184
145
class UnionProperty (Property ):
185
146
""" A property representing a Union (anyOf) of other properties """
186
147
@@ -214,41 +175,6 @@ def get_imports(self, *, prefix: str) -> Set[str]:
214
175
imports .add ("from typing import Union" )
215
176
return imports
216
177
217
- def _validate_default (self , default : Any ) -> Any :
218
- for property in self .inner_properties :
219
- try :
220
- val = property ._validate_default (default )
221
- return val
222
- except ValidationError :
223
- continue
224
- raise ValidationError ()
225
-
226
-
227
- @dataclass
228
- class DictProperty (Property ):
229
- """ Property that is a general Dict """
230
-
231
- _type_string : ClassVar [str ] = "Dict[Any, Any]"
232
- template : ClassVar [str ] = "dict_property.pyi"
233
-
234
- def get_imports (self , * , prefix : str ) -> Set [str ]:
235
- """
236
- Get a set of import strings that should be included when this property is used somewhere
237
-
238
- Args:
239
- prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
240
- back to the root of the generated client.
241
- """
242
- imports = super ().get_imports (prefix = prefix )
243
- imports .add ("from typing import Dict" )
244
- if self .default is not None :
245
- imports .add ("from dataclasses import field" )
246
- imports .add ("from typing import cast" )
247
- return imports
248
-
249
- def _validate_default (self , default : Any ) -> None :
250
- return None
251
-
252
178
253
179
def _string_based_property (
254
180
name : str , required : bool , data : oai .Schema
@@ -259,27 +185,27 @@ def _string_based_property(
259
185
return DateTimeProperty (
260
186
name = name ,
261
187
required = required ,
262
- default = data .default ,
188
+ default = convert ( "datetime.datetime" , data .default ) ,
263
189
nullable = data .nullable ,
264
190
)
265
191
elif string_format == "date" :
266
192
return DateProperty (
267
193
name = name ,
268
194
required = required ,
269
- default = data .default ,
195
+ default = convert ( "datetime.date" , data .default ) ,
270
196
nullable = data .nullable ,
271
197
)
272
198
elif string_format == "binary" :
273
199
return FileProperty (
274
200
name = name ,
275
201
required = required ,
276
- default = data . default ,
202
+ default = None ,
277
203
nullable = data .nullable ,
278
204
)
279
205
else :
280
206
return StringProperty (
281
207
name = name ,
282
- default = data .default ,
208
+ default = convert ( "str" , data .default ) ,
283
209
required = required ,
284
210
pattern = data .pattern ,
285
211
nullable = data .nullable ,
@@ -327,23 +253,54 @@ def build_model_property(
327
253
required = required ,
328
254
name = name ,
329
255
)
330
- schemas = replace (schemas , models = {** schemas .models , prop .reference .class_name : prop })
256
+ schemas = attr . evolve (schemas , models = {** schemas .models , prop .reference .class_name : prop })
331
257
return prop , schemas
332
258
333
259
334
260
def build_enum_property (
335
261
* , data : oai .Schema , name : str , required : bool , schemas : Schemas , enum : List [Union [str , int ]]
336
- ) -> Tuple [EnumProperty , Schemas ]:
262
+ ) -> Tuple [Union [EnumProperty , PropertyError ], Schemas ]:
263
+
264
+ reference = Reference .from_ref (data .title or name )
265
+ values = EnumProperty .values_from_list (enum )
266
+
267
+ dedup_counter = 0 # TODO: use the parent names instead of a counter for deduping
268
+ while reference .class_name in schemas .enums :
269
+ existing = schemas .enums [reference .class_name ]
270
+ if values == existing .values :
271
+ break # This is the same Enum, we're good
272
+ dedup_counter += 1
273
+ reference = Reference .from_ref (f"{ reference .class_name } { dedup_counter } " )
274
+
275
+ for value in values .values ():
276
+ value_type = type (value )
277
+ break
278
+ else :
279
+ return PropertyError (data = data , detail = "No values provided for Enum" ), schemas
280
+
281
+ default = None
282
+ if data .default is not None :
283
+ inverse_values = {v : k for k , v in values .items ()}
284
+ try :
285
+ default = f"{ reference .class_name } .{ inverse_values [data .default ]} "
286
+ except KeyError :
287
+ return (
288
+ PropertyError (
289
+ detail = f"{ data .default } is an invalid default for enum { reference .class_name } " , data = data
290
+ ),
291
+ schemas ,
292
+ )
293
+
337
294
prop = EnumProperty (
338
295
name = name ,
339
296
required = required ,
340
- values = EnumProperty .values_from_list (enum ),
341
- title = data .title or name ,
342
- default = data .default ,
297
+ default = default ,
343
298
nullable = data .nullable ,
344
- existing_enums = schemas .enums ,
299
+ reference = reference ,
300
+ values = values ,
301
+ value_type = value_type ,
345
302
)
346
- schemas = replace (schemas , enums = {** schemas .enums , prop .reference .class_name : prop })
303
+ schemas = attr . evolve (schemas , enums = {** schemas .enums , prop .reference .class_name : prop })
347
304
return prop , schemas
348
305
349
306
@@ -356,11 +313,13 @@ def build_union_property(
356
313
if isinstance (sub_prop , PropertyError ):
357
314
return PropertyError (detail = f"Invalid property in union { name } " , data = sub_prop_data ), schemas
358
315
sub_properties .append (sub_prop )
316
+
317
+ default = convert_chain ((prop ._type_string for prop in sub_properties ), data .default )
359
318
return (
360
319
UnionProperty (
361
320
name = name ,
362
321
required = required ,
363
- default = data . default ,
322
+ default = default ,
364
323
inner_properties = sub_properties ,
365
324
nullable = data .nullable ,
366
325
),
@@ -380,7 +339,7 @@ def build_list_property(
380
339
ListProperty (
381
340
name = name ,
382
341
required = required ,
383
- default = data . default ,
342
+ default = None ,
384
343
inner_property = inner_prop ,
385
344
nullable = data .nullable ,
386
345
),
@@ -398,29 +357,27 @@ def _property_from_data(
398
357
name = utils .remove_string_escapes (name )
399
358
if isinstance (data , oai .Reference ):
400
359
reference = Reference .from_ref (data .ref )
401
- if reference .class_name in schemas .enums :
402
- existing = schemas . enums [ reference . class_name ]
360
+ existing = schemas . enums . get ( reference .class_name ) or schemas .models . get ( reference . class_name )
361
+ if existing :
403
362
return (
404
- replace (existing , required = required , name = name , title = reference . class_name , existing_enums = {} ),
363
+ attr . evolve (existing , required = required , name = name ),
405
364
schemas ,
406
365
)
407
- elif reference .class_name in schemas .models :
408
- return replace (schemas .models [reference .class_name ], required = required , name = name ), schemas
409
- else :
410
- return PropertyError (data = data , detail = "Could not find reference in parsed models or enums" ), schemas
366
+ return PropertyError (data = data , detail = "Could not find reference in parsed models or enums" ), schemas
411
367
if data .enum :
412
368
return build_enum_property (data = data , name = name , required = required , schemas = schemas , enum = data .enum )
413
369
if data .anyOf or data .oneOf :
414
370
return build_union_property (data = data , name = name , required = required , schemas = schemas )
415
371
if not data .type :
416
372
return PropertyError (data = data , detail = "Schemas must either have one of enum, anyOf, or type defined." ), schemas
373
+
417
374
if data .type == "string" :
418
375
return _string_based_property (name = name , required = required , data = data ), schemas
419
376
elif data .type == "number" :
420
377
return (
421
378
FloatProperty (
422
379
name = name ,
423
- default = data .default ,
380
+ default = convert ( "float" , data .default ) ,
424
381
required = required ,
425
382
nullable = data .nullable ,
426
383
),
@@ -430,7 +387,7 @@ def _property_from_data(
430
387
return (
431
388
IntProperty (
432
389
name = name ,
433
- default = data .default ,
390
+ default = convert ( "int" , data .default ) ,
434
391
required = required ,
435
392
nullable = data .nullable ,
436
393
),
@@ -441,7 +398,7 @@ def _property_from_data(
441
398
BooleanProperty (
442
399
name = name ,
443
400
required = required ,
444
- default = data .default ,
401
+ default = convert ( "bool" , data .default ) ,
445
402
nullable = data .nullable ,
446
403
),
447
404
schemas ,
0 commit comments