22import datetime as dt
33import json
44from io import BytesIO
5- from typing import ClassVar , Dict , Generator , Optional , Union
5+ from typing import Any , ClassVar , Generator , Optional , Type , TypeVar , cast
66from urllib .parse import urlencode
77
88from cuenca_validations .types import (
1212 TransactionQuery ,
1313 TransactionStatus ,
1414)
15- from pydantic import BaseModel
15+ from pydantic import BaseModel , Extra
1616
1717from ..exc import MultipleResultsFound , NoResultFound
1818from ..http import Session , session as global_session
1919
20+ R_co = TypeVar ('R_co' , bound = 'Resource' , covariant = True )
21+
2022
2123class Resource (BaseModel ):
2224 _resource : ClassVar [str ]
2325
2426 id : str
2527
26- @classmethod
27- def _from_dict (cls , obj_dict : Dict [str , Union [str , int ]]) -> 'Resource' :
28- cls ._filter_excess_fields (obj_dict )
29- return cls (** obj_dict )
30-
31- @classmethod
32- def _filter_excess_fields (cls , obj_dict ):
33- """
34- dataclasses don't allow __init__ to be called with excess fields. This
35- method allows the API to add fields in the response body without
36- breaking the client
37- """
38- excess = set (obj_dict .keys ()) - set (
39- cls .schema ().get ("properties" ).keys ()
40- )
41- for f in excess :
42- del obj_dict [f ]
28+ class Config :
29+ extra = Extra .ignore
4330
4431 def to_dict (self ):
4532 return SantizedDict (self .dict ())
@@ -48,22 +35,30 @@ def to_dict(self):
4835class Retrievable (Resource ):
4936 @classmethod
5037 def retrieve (
51- cls , id : str , * , session : Session = global_session
52- ) -> Resource :
38+ cls : Type [R_co ],
39+ id : str ,
40+ * ,
41+ session : Session = global_session ,
42+ ) -> R_co :
5343 resp = session .get (f'/{ cls ._resource } /{ id } ' )
54- return cls . _from_dict ( resp )
44+ return cls ( ** resp )
5545
56- def refresh (self , * , session : Session = global_session ):
46+ def refresh (self , * , session : Session = global_session ) -> None :
5747 new = self .retrieve (self .id , session = session )
5848 for attr , value in new .__dict__ .items ():
5949 setattr (self , attr , value )
6050
6151
6252class Creatable (Resource ):
6353 @classmethod
64- def _create (cls , * , session : Session = global_session , ** data ) -> Resource :
54+ def _create (
55+ cls : Type [R_co ],
56+ * ,
57+ session : Session = global_session ,
58+ ** data : Any ,
59+ ) -> R_co :
6560 resp = session .post (cls ._resource , data )
66- return cls . _from_dict ( resp )
61+ return cls ( ** resp )
6762
6863
6964class Updateable (Resource ):
@@ -72,31 +67,39 @@ class Updateable(Resource):
7267
7368 @classmethod
7469 def _update (
75- cls , id : str , * , session : Session = global_session , ** data
76- ) -> Resource :
70+ cls : Type [R_co ],
71+ id : str ,
72+ * ,
73+ session : Session = global_session ,
74+ ** data : Any ,
75+ ) -> R_co :
7776 resp = session .patch (f'/{ cls ._resource } /{ id } ' , data )
78- return cls . _from_dict ( resp )
77+ return cls ( ** resp )
7978
8079
8180class Deactivable (Resource ):
8281 deactivated_at : Optional [dt .datetime ]
8382
8483 @classmethod
8584 def deactivate (
86- cls , id : str , * , session : Session = global_session , ** data
87- ) -> Resource :
85+ cls : Type [R_co ],
86+ id : str ,
87+ * ,
88+ session : Session = global_session ,
89+ ** data : Any ,
90+ ) -> R_co :
8891 resp = session .delete (f'/{ cls ._resource } /{ id } ' , data )
89- return cls . _from_dict ( resp )
92+ return cls ( ** resp )
9093
9194 @property
92- def is_active (self ):
95+ def is_active (self ) -> bool :
9396 return not self .deactivated_at
9497
9598
9699class Downloadable (Resource ):
97100 @classmethod
98101 def download (
99- cls ,
102+ cls : Type [ R_co ] ,
100103 id : str ,
101104 file_format : FileFormat = FileFormat .any ,
102105 * ,
@@ -121,13 +124,13 @@ def xml(self) -> bytes:
121124class Uploadable (Resource ):
122125 @classmethod
123126 def _upload (
124- cls ,
127+ cls : Type [ R_co ] ,
125128 file : bytes ,
126129 user_id : str ,
127130 * ,
128131 session : Session = global_session ,
129- ** data ,
130- ) -> Resource :
132+ ** data : Any ,
133+ ) -> R_co :
131134 encoded_file = base64 .b64encode (file )
132135 resp = session .request (
133136 'post' ,
@@ -138,7 +141,7 @@ def _upload(
138141 ** {k : (None , v ) for k , v in data .items ()},
139142 ),
140143 )
141- return cls . _from_dict ( json .loads (resp ))
144+ return cls ( ** json .loads (resp ))
142145
143146
144147class Queryable (Resource ):
@@ -148,50 +151,62 @@ class Queryable(Resource):
148151
149152 @classmethod
150153 def one (
151- cls , * , session : Session = global_session , ** query_params
152- ) -> Resource :
153- q = cls ._query_params (limit = 2 , ** query_params )
154+ cls : Type [R_co ],
155+ * ,
156+ session : Session = global_session ,
157+ ** query_params : Any ,
158+ ) -> R_co :
159+ q = cast (Queryable , cls )._query_params (limit = 2 , ** query_params )
154160 resp = session .get (cls ._resource , q .dict ())
155161 items = resp ['items' ]
156162 len_items = len (items )
157163 if not len_items :
158164 raise NoResultFound
159165 if len_items > 1 :
160166 raise MultipleResultsFound
161- return cls . _from_dict ( items [0 ])
167+ return cls ( ** items [0 ])
162168
163169 @classmethod
164170 def first (
165- cls , * , session : Session = global_session , ** query_params
166- ) -> Optional [Resource ]:
167- q = cls ._query_params (limit = 1 , ** query_params )
171+ cls : Type [R_co ],
172+ * ,
173+ session : Session = global_session ,
174+ ** query_params : Any ,
175+ ) -> Optional [R_co ]:
176+ q = cast (Queryable , cls )._query_params (limit = 1 , ** query_params )
168177 resp = session .get (cls ._resource , q .dict ())
169178 try :
170179 item = resp ['items' ][0 ]
171180 except IndexError :
172181 rv = None
173182 else :
174- rv = cls . _from_dict ( item )
183+ rv = cls ( ** item )
175184 return rv
176185
177186 @classmethod
178187 def count (
179- cls , * , session : Session = global_session , ** query_params
188+ cls : Type [R_co ],
189+ * ,
190+ session : Session = global_session ,
191+ ** query_params : Any ,
180192 ) -> int :
181- q = cls ._query_params (count = True , ** query_params )
193+ q = cast ( Queryable , cls ) ._query_params (count = True , ** query_params )
182194 resp = session .get (cls ._resource , q .dict ())
183195 return resp ['count' ]
184196
185197 @classmethod
186198 def all (
187- cls , * , session : Session = global_session , ** query_params
188- ) -> Generator [Resource , None , None ]:
199+ cls : Type [R_co ],
200+ * ,
201+ session : Session = global_session ,
202+ ** query_params : Any ,
203+ ) -> Generator [R_co , None , None ]:
189204 session = session or global_session
190- q = cls ._query_params (** query_params )
205+ q = cast ( Queryable , cls ) ._query_params (** query_params )
191206 next_page_uri = f'{ cls ._resource } ?{ urlencode (q .dict ())} '
192207 while next_page_uri :
193208 page = session .get (next_page_uri )
194- yield from (cls . _from_dict ( item ) for item in page ['items' ])
209+ yield from (cls ( ** item ) for item in page ['items' ])
195210 next_page_uri = page ['next_page_uri' ]
196211
197212
0 commit comments