11from  abc  import  ABC 
22from  typing  import  Any , ClassVar , Self , override 
3+ from  urllib .parse  import  urljoin 
34
45from  httpx  import  Response 
56
6- from  mpt_api_client .http .client  import  HTTPClient 
7+ from  mpt_api_client .http .client  import  HTTPClient ,  HTTPClientAsync 
78from  mpt_api_client .models  import  Resource 
9+ from  mpt_api_client .models .base  import  ResourceData , ResourceList 
810
911
10- class  ResourceBaseClient [ ResourceModel :  Resource ]( ABC ):   # noqa: WPS214 
11-     """Client  for RESTful resources .""" 
12+ class  ResourceMixin : 
13+     """Mixin  for resource clients .""" 
1214
1315    _endpoint : str 
14-     _resource_class : type [ResourceModel ]
16+     _resource_class : type [Any ]
1517    _safe_attributes : ClassVar [set [str ]] =  {"http_client_" , "resource_id_" , "resource_" }
1618
17-     def  __init__ (self , http_client : HTTPClient , resource_id : str ) ->  None :
18-         self .http_client_  =  http_client   # noqa: WPS120 
19-         self .resource_id_  =  resource_id   # noqa: WPS120 
20-         self .resource_ : Resource  |  None  =  None   # noqa: WPS120 
19+     def  __init__ (
20+         self , http_client : HTTPClient  |  HTTPClientAsync , resource_id : str , resource : Resource  |  None 
21+     ) ->  None :
22+         self .http_client_  =  http_client 
23+         self .resource_id_  =  resource_id 
24+         self .resource_ : Resource  |  None  =  resource 
2125
2226    def  __getattr__ (self , attribute : str ) ->  Any :
2327        """Returns the resource data.""" 
24-         self ._ensure_resource_is_fetched ()
28+         self ._assert_resource_is_set ()
2529        return  self .resource_ .__getattr__ (attribute )  # type: ignore[union-attr] 
2630
2731    @property  
@@ -34,9 +38,33 @@ def __setattr__(self, attribute: str, attribute_value: Any) -> None:
3438        if  attribute  in  self ._safe_attributes :
3539            object .__setattr__ (self , attribute , attribute_value )
3640            return 
37-         self ._ensure_resource_is_fetched ()
41+         self ._assert_resource_is_set ()
3842        self .resource_ .__setattr__ (attribute , attribute_value )
3943
44+     def  _assert_resource_is_set (self ) ->  None :
45+         if  not  self .resource_ :
46+             class_name  =  self ._resource_class .__name__ 
47+             raise  RuntimeError (
48+                 f"Resource data not available. Call fetch() method first to retrieve" 
49+                 f" the resource `{ class_name }  
50+             )
51+ 
52+ 
53+ class  ResourceBaseClient [ResourceModel : Resource ](ABC , ResourceMixin ):
54+     """Client for RESTful resources.""" 
55+ 
56+     _endpoint : str 
57+     _resource_class : type [ResourceModel ]
58+     _safe_attributes : ClassVar [set [str ]] =  {"http_client_" , "resource_id_" , "resource_" }
59+ 
60+     def  __init__ (
61+         self , http_client : HTTPClient , resource_id : str , resource : Resource  |  None  =  None 
62+     ) ->  None :
63+         self .http_client_ : HTTPClient  =  http_client   # type: ignore[mutable-override] 
64+         ResourceMixin .__init__ (
65+             self , http_client = http_client , resource_id = resource_id , resource = resource 
66+         )
67+ 
4068    def  fetch (self ) ->  ResourceModel :
4169        """Fetch a specific resource using `GET /endpoint/{resource_id}`. 
4270
@@ -45,27 +73,74 @@ def fetch(self) -> ResourceModel:
4573        Returns: 
4674            The fetched resource. 
4775        """ 
48-         response  =  self .do_action ("GET" )
76+         response  =  self ._do_action ("GET" )
4977
50-         self .resource_  =  self ._resource_class .from_response (response )   # noqa: WPS120 
78+         self .resource_  =  self ._resource_class .from_response (response )
5179        return  self .resource_ 
5280
53-     def  resource_action (
81+     def  update (self , resource_data : ResourceData ) ->  ResourceModel :
82+         """Update a specific in the API and catches the result as a current resource. 
83+ 
84+         Args: 
85+             resource_data: The updated resource data. 
86+ 
87+         Returns: 
88+             The updated resource. 
89+ 
90+         Examples: 
91+             updated_contact = contact.update({"name": "New Name"}) 
92+ 
93+ 
94+         """ 
95+         response  =  self ._do_action ("PUT" , json = resource_data )
96+         self .resource_  =  self ._resource_class .from_response (response )
97+         return  self .resource_ 
98+ 
99+     def  save (self ) ->  Self :
100+         """Save the current state of the resource to the api using the update method. 
101+ 
102+         Raises: 
103+             ValueError: If the resource has not been set. 
104+ 
105+         Examples: 
106+             contact.name = "New Name" 
107+             contact.save() 
108+ 
109+         """ 
110+         self ._assert_resource_is_set ()
111+         self .update (self .resource_ .to_dict ())  # type: ignore[union-attr] 
112+         return  self 
113+ 
114+     def  delete (self ) ->  None :
115+         """Delete the resource using `DELETE /endpoint/{resource_id}`. 
116+ 
117+         Raises: 
118+             HTTPStatusError: If the deletion fails. 
119+ 
120+         Examples: 
121+             contact.delete() 
122+         """ 
123+         response  =  self ._do_action ("DELETE" )
124+         response .raise_for_status ()
125+ 
126+         self .resource_  =  None 
127+ 
128+     def  _resource_action (
54129        self ,
55130        method : str  =  "GET" ,
56131        url : str  |  None  =  None ,
57-         json : dict [ str ,  Any ]  |   list [ Any ]  |  None  =  None ,   # noqa: WPS221 
132+         json : ResourceData   |   ResourceList  |  None  =  None ,
58133    ) ->  ResourceModel :
59134        """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.""" 
60-         response  =  self .do_action (method , url , json = json )
61-         self .resource_  =  self ._resource_class .from_response (response )   # noqa: WPS120 
135+         response  =  self ._do_action (method , url , json = json )
136+         self .resource_  =  self ._resource_class .from_response (response )
62137        return  self .resource_ 
63138
64-     def  do_action (
139+     def  _do_action (
65140        self ,
66141        method : str  =  "GET" ,
67142        url : str  |  None  =  None ,
68-         json : dict [ str ,  Any ]  |   list [ Any ]  |  None  =  None ,   # noqa: WPS221 
143+         json : ResourceData   |   ResourceList  |  None  =  None ,
69144    ) ->  Response :
70145        """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`. 
71146
@@ -82,7 +157,36 @@ def do_action(
82157        response .raise_for_status ()
83158        return  response 
84159
85-     def  update (self , resource_data : dict [str , Any ]) ->  ResourceModel :
160+ 
161+ class  AsyncResourceBaseClient [ResourceModel : Resource ](ABC , ResourceMixin ):
162+     """Client for RESTful resources.""" 
163+ 
164+     _endpoint : str 
165+     _resource_class : type [ResourceModel ]
166+     _safe_attributes : ClassVar [set [str ]] =  {"http_client_" , "resource_id_" , "resource_" }
167+ 
168+     def  __init__ (
169+         self , http_client : HTTPClientAsync , resource_id : str , resource : Resource  |  None  =  None 
170+     ) ->  None :
171+         self .http_client_ : HTTPClientAsync  =  http_client   # type: ignore[mutable-override] 
172+         ResourceMixin .__init__ (
173+             self , http_client = http_client , resource_id = resource_id , resource = resource 
174+         )
175+ 
176+     async  def  fetch (self ) ->  ResourceModel :
177+         """Fetch a specific resource using `GET /endpoint/{resource_id}`. 
178+ 
179+         It fetches and caches the resource. 
180+ 
181+         Returns: 
182+             The fetched resource. 
183+         """ 
184+         response  =  await  self ._do_action ("GET" )
185+ 
186+         self .resource_  =  self ._resource_class .from_response (response )
187+         return  self .resource_ 
188+ 
189+     async  def  update (self , resource_data : ResourceData ) ->  ResourceModel :
86190        """Update a specific in the API and catches the result as a current resource. 
87191
88192        Args: 
@@ -96,11 +200,9 @@ def update(self, resource_data: dict[str, Any]) -> ResourceModel:
96200
97201
98202        """ 
99-         response  =  self .do_action ("PUT" , json = resource_data )
100-         self .resource_  =  self ._resource_class .from_response (response )  # noqa: WPS120 
101-         return  self .resource_ 
203+         return  await  self ._resource_action ("PUT" , json = resource_data )
102204
103-     def  save (self ) ->  Self :
205+     async   def  save (self ) ->  Self :
104206        """Save the current state of the resource to the api using the update method. 
105207
106208        Raises: 
@@ -111,12 +213,11 @@ def save(self) -> Self:
111213            contact.save() 
112214
113215        """ 
114-         if  not  self .resource_ :
115-             raise  ValueError ("Unable to save resource that has not been set." )
116-         self .update (self .resource_ .to_dict ())
216+         self ._assert_resource_is_set ()
217+         await  self .update (self .resource_ .to_dict ())  # type: ignore[union-attr] 
117218        return  self 
118219
119-     def  delete (self ) ->  None :
220+     async   def  delete (self ) ->  None :
120221        """Delete the resource using `DELETE /endpoint/{resource_id}`. 
121222
122223        Raises: 
@@ -125,11 +226,39 @@ def delete(self) -> None:
125226        Examples: 
126227            contact.delete() 
127228        """ 
128-         response  =  self .do_action ("DELETE" )
229+         response  =  await   self ._do_action ("DELETE" )
129230        response .raise_for_status ()
130231
131-         self .resource_  =  None    # noqa: WPS120 
232+         self .resource_  =  None 
132233
133-     def  _ensure_resource_is_fetched (self ) ->  None :
134-         if  not  self .resource_ :
135-             self .fetch ()
234+     async  def  _resource_action (
235+         self ,
236+         method : str  =  "GET" ,
237+         url : str  |  None  =  None ,
238+         json : ResourceData  |  ResourceList  |  None  =  None ,
239+     ) ->  ResourceModel :
240+         """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`.""" 
241+         response  =  await  self ._do_action (method , url , json = json )
242+         self .resource_  =  self ._resource_class .from_response (response )
243+         return  self .resource_ 
244+ 
245+     async  def  _do_action (
246+         self ,
247+         method : str  =  "GET" ,
248+         url : str  |  None  =  None ,
249+         json : ResourceData  |  ResourceList  |  None  =  None ,
250+     ) ->  Response :
251+         """Perform an action on a specific resource using `HTTP_METHOD /endpoint/{resource_id}`. 
252+ 
253+         Args: 
254+             method: The HTTP method to use. 
255+             url: The action name to use. 
256+             json: The updated resource data. 
257+ 
258+         Raises: 
259+             HTTPError: If the action fails. 
260+         """ 
261+         url  =  urljoin (self .resource_url , url ) if  url  else  self .resource_url 
262+         response  =  await  self .http_client_ .request (method , url , json = json )
263+         response .raise_for_status ()
264+         return  response 
0 commit comments