1616from pydantic import BaseModel
1717
1818from .outputs import ClientBlobOutput
19+ from ..exceptions import (
20+ FailedToInvokeActionError ,
21+ ServerActionError ,
22+ ClientPropertyError ,
23+ )
1924
2025__all__ = ["ThingClient" , "poll_invocation" ]
2126ACTION_RUNNING_KEYWORDS = ["idle" , "pending" , "running" ]
@@ -143,10 +148,17 @@ def get_property(self, path: str) -> Any:
143148 to the ``base_url``.
144149
145150 :return: the property's value, as deserialised from JSON.
151+ :raise ClientPropertyError: is raised the property cannot be read.
146152 """
147- r = self .client .get (urljoin (self .path , path ))
148- r .raise_for_status ()
149- return r .json ()
153+ response = self .client .get (urljoin (self .path , path ))
154+ if response .is_error :
155+ detail = response .json ().get ("detail" )
156+ err_msg = "Unknown error"
157+ if isinstance (detail , str ):
158+ err_msg = detail
159+ raise ClientPropertyError (f"Failed to get property { path } : { err_msg } " )
160+
161+ return response .json ()
150162
151163 def set_property (self , path : str , value : Any ) -> None :
152164 """Make a PUT request to set the value of a property.
@@ -155,9 +167,20 @@ def set_property(self, path: str, value: Any) -> None:
155167 to the ``base_url``.
156168 :param value: the property's value. Currently this must be
157169 serialisable to JSON.
170+ :raise ClientPropertyError: is raised the property cannot be set.
158171 """
159- r = self .client .put (urljoin (self .path , path ), json = value )
160- r .raise_for_status ()
172+ response = self .client .put (urljoin (self .path , path ), json = value )
173+ if response .is_error :
174+ detail = response .json ().get ("detail" )
175+ err_msg = "Unknown error"
176+ if isinstance (detail , str ):
177+ err_msg = detail
178+ elif (
179+ isinstance (detail , list ) and len (detail ) and isinstance (detail [0 ], dict )
180+ ):
181+ err_msg = detail [0 ].get ("msg" , "Unknown error" )
182+
183+ raise ClientPropertyError (f"Failed to get property { path } : { err_msg } " )
161184
162185 def invoke_action (self , path : str , ** kwargs : Any ) -> Any :
163186 r"""Invoke an action on the Thing.
@@ -177,7 +200,9 @@ def invoke_action(self, path: str, **kwargs: Any) -> Any:
177200
178201 :return: the output value of the action.
179202
180- :raise RuntimeError: is raised if the action does not complete successfully.
203+ :raise FailedToInvokeActionError: if the action fails to start.
204+ :raise ServerActionError: is raised if the action does not complete
205+ successfully.
181206 """
182207 for k in kwargs .keys ():
183208 value = kwargs [k ]
@@ -191,9 +216,12 @@ def invoke_action(self, path: str, **kwargs: Any) -> Any:
191216 # Note that the blob will not be uploaded: we rely on the blob
192217 # still existing on the server.
193218 kwargs [k ] = {"href" : value .href , "media_type" : value .media_type }
194- r = self .client .post (urljoin (self .path , path ), json = kwargs )
195- r .raise_for_status ()
196- invocation = poll_invocation (self .client , r .json ())
219+ response = self .client .post (urljoin (self .path , path ), json = kwargs )
220+ if response .is_error :
221+ message = _construct_failed_to_invoke_message (path , response )
222+ raise FailedToInvokeActionError (message )
223+
224+ invocation = poll_invocation (self .client , response .json ())
197225 if invocation ["status" ] == "completed" :
198226 if (
199227 isinstance (invocation ["output" ], Mapping )
@@ -206,8 +234,8 @@ def invoke_action(self, path: str, **kwargs: Any) -> Any:
206234 client = self .client ,
207235 )
208236 return invocation ["output" ]
209- else :
210- raise RuntimeError ( f"Action did not complete successfully: { invocation } " )
237+ message = _construct_invocation_error_message ( invocation )
238+ raise ServerActionError ( message )
211239
212240 def follow_link (self , response : dict , rel : str ) -> httpx .Response :
213241 """Follow a link in a response object, by its `rel` attribute.
@@ -398,3 +426,52 @@ def add_property(cls: type[ThingClient], property_name: str, property: dict) ->
398426 readable = not property .get ("writeOnly" , False ),
399427 ),
400428 )
429+
430+
431+ def _construct_failed_to_invoke_message (path : str , response : httpx .Response ) -> str :
432+ """Format an error for ThingClient to raise if an invocation fails to start.
433+
434+ :param path: The path of the action
435+ :param response: The response object from the POST request to start the action.
436+ :return: The message for the raised error
437+ """
438+ # Default message if we can't process return
439+ message = f"Unknown error when invoking action { path } "
440+ details = response .json ().get ("detail" , [])
441+
442+ if isinstance (details , str ):
443+ message = f"Error when invoking action { path } : { details } "
444+ if isinstance (details , list ) and len (details ) and isinstance (details [0 ], dict ):
445+ loc = details [0 ].get ("loc" , [])
446+ loc_str = "" if len (loc ) < 2 else f"'{ loc [1 ]} ' - "
447+ err_msg = details [0 ].get ("msg" , "Unknown Error" )
448+ message = f"Error when invoking action { path } : { loc_str } { err_msg } "
449+ return message
450+
451+
452+ def _construct_invocation_error_message (invocation : Mapping [str , Any ]) -> str :
453+ """Format an error for ThingClient to raise if an invocation ends in and error.
454+
455+ :param invocation: The invocation dictionary returned.
456+ :return: The message for the raised error
457+ """
458+ inv_id = invocation ["id" ]
459+ action_name = invocation ["action" ].split ("/" )[- 1 ]
460+
461+ err_message = "Unknown error"
462+
463+ if len (invocation .get ("log" , [])) > 0 :
464+ last_log = invocation ["log" ][- 1 ]
465+ err_message = last_log .get ("message" , err_message )
466+
467+ exception_type = last_log .get ("exception_type" )
468+ if exception_type is not None :
469+ err_message = f"[{ exception_type } ]: { err_message } "
470+
471+ traceback = last_log .get ("traceback" )
472+ if traceback is not None :
473+ err_message += "\n \n SERVER TRACEBACK START:\n \n "
474+ err_message += traceback
475+ err_message += "\n \n SERVER TRACEBACK END\n \n "
476+
477+ return f"Action { action_name } (ID: { inv_id } ) failed with error:\n { err_message } "
0 commit comments