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