1010
1111from .http import HttpClient
1212from .odata_upload_files import ODataFileUpload
13- from .errors import HttpError
13+ from .errors import *
1414from . import error_codes as ec
1515
1616
@@ -76,45 +76,53 @@ def _raw_request(self, method: str, url: str, **kwargs):
7676 return self ._http .request (method , url , ** kwargs )
7777
7878 def _request (self , method : str , url : str , * , expected : tuple [int , ...] = (200 , 201 , 202 , 204 ), ** kwargs ):
79- """Execute HTTP request; raise HttpError with structured details on failure.
80-
81- Returns the raw response for success codes; raises HttpError with extracted
82- Dataverse error payload fields and correlation identifiers otherwise.
83- """
84- headers = kwargs .pop ("headers" , None )
85- kwargs ["headers" ] = self ._merge_headers (headers )
79+ headers_in = kwargs .pop ("headers" , None )
80+ kwargs ["headers" ] = self ._merge_headers (headers_in )
8681 r = self ._raw_request (method , url , ** kwargs )
8782 if r .status_code in expected :
8883 return r
89- payload = {}
84+ headers = getattr (r , "headers" , {}) or {}
85+ body_excerpt = (getattr (r , "text" , "" ) or "" )[:200 ]
86+ svc_code = None
87+ msg = f"HTTP { r .status_code } "
9088 try :
91- payload = r .json () if getattr (r , 'text' , None ) else {}
89+ data = r .json () if getattr (r , "text" , None ) else {}
90+ if isinstance (data , dict ):
91+ inner = data .get ("error" )
92+ if isinstance (inner , dict ):
93+ svc_code = inner .get ("code" )
94+ imsg = inner .get ("message" )
95+ if isinstance (imsg , str ) and imsg .strip ():
96+ msg = imsg .strip ()
97+ else :
98+ imsg2 = data .get ("message" )
99+ if isinstance (imsg2 , str ) and imsg2 .strip ():
100+ msg = imsg2 .strip ()
92101 except Exception :
93- payload = {}
94- svc_err = payload .get ("error" ) if isinstance (payload , dict ) else None
95- svc_code = svc_err .get ("code" ) if isinstance (svc_err , dict ) else None
96- svc_msg = svc_err .get ("message" ) if isinstance (svc_err , dict ) else None
97- message = svc_msg or f"HTTP { r .status_code } "
98- subcode = f"http_{ r .status_code } "
99-
100- headers = getattr (r , 'headers' , {}) or {}
101- details = {
102- "service_error_code" : svc_code ,
103- "body_excerpt" : (getattr (r , 'text' , '' ) or '' )[:200 ],
104- "correlation_id" : headers .get ("x-ms-correlation-request-id" ) or headers .get ("x-ms-correlation-id" ),
105- "request_id" : headers .get ("x-ms-client-request-id" ) or headers .get ("request-id" ),
106- "traceparent" : headers .get ("traceparent" ),
107- }
102+ pass
103+ sc = r .status_code
104+ subcode = ec .http_subcode (sc )
105+ correlation_id = headers .get ("x-ms-correlation-request-id" ) or headers .get ("x-ms-correlation-id" )
106+ request_id = headers .get ("x-ms-client-request-id" ) or headers .get ("request-id" ) or headers .get ("x-ms-request-id" )
107+ traceparent = headers .get ("traceparent" )
108108 ra = headers .get ("Retry-After" )
109+ retry_after = None
109110 if ra :
110- details ["retry_after" ] = ra
111- is_transient = r .status_code in (429 , 502 , 503 , 504 )
111+ try :
112+ retry_after = int (ra )
113+ except Exception :
114+ retry_after = None
115+ is_transient = ec .is_transient_status (sc )
112116 raise HttpError (
113- message ,
117+ msg ,
118+ status_code = sc ,
114119 subcode = subcode ,
115- status_code = r .status_code ,
116- details = details ,
117- source = {"method" : method , "url" : url },
120+ service_error_code = svc_code ,
121+ correlation_id = correlation_id ,
122+ request_id = request_id ,
123+ traceparent = traceparent ,
124+ body_excerpt = body_excerpt ,
125+ retry_after = retry_after ,
118126 is_transient = is_transient ,
119127 )
120128
@@ -500,8 +508,10 @@ def _query_sql(self, sql: str) -> list[dict[str, Any]]:
500508 RuntimeError
501509 If metadata lookup for the logical name fails.
502510 """
503- if not isinstance (sql , str ) or not sql .strip ():
504- raise ValueError ("sql must be a non-empty string" )
511+ if not isinstance (sql , str ):
512+ raise ValidationError ("sql must be a string" , subcode = ec .VALIDATION_SQL_NOT_STRING )
513+ if not sql .strip ():
514+ raise ValidationError ("sql must be a non-empty string" , subcode = ec .VALIDATION_SQL_EMPTY )
505515 sql = sql .strip ()
506516
507517 # Extract logical table name via helper (robust to identifiers ending with 'from')
@@ -570,11 +580,17 @@ def _entity_set_from_logical(self, logical: str) -> str:
570580 items = []
571581 if not items :
572582 plural_hint = " (did you pass a plural entity set name instead of the singular logical name?)" if logical .endswith ("s" ) and not logical .endswith ("ss" ) else ""
573- raise RuntimeError (f"Unable to resolve entity set for logical name '{ logical } '. Provide the singular logical name.{ plural_hint } " )
583+ raise MetadataError (
584+ f"Unable to resolve entity set for logical name '{ logical } '. Provide the singular logical name.{ plural_hint } " ,
585+ subcode = ec .METADATA_ENTITYSET_NOT_FOUND ,
586+ )
574587 md = items [0 ]
575588 es = md .get ("EntitySetName" )
576589 if not es :
577- raise RuntimeError (f"Metadata response missing EntitySetName for logical '{ logical } '." )
590+ raise MetadataError (
591+ f"Metadata response missing EntitySetName for logical '{ logical } '." ,
592+ subcode = ec .METADATA_ENTITYSET_NAME_MISSING ,
593+ )
578594 self ._logical_to_entityset_cache [logical ] = es
579595 primary_id_attr = md .get ("PrimaryIdAttribute" )
580596 if isinstance (primary_id_attr , str ) and primary_id_attr :
@@ -1014,7 +1030,10 @@ def _delete_table(self, tablename: str) -> None:
10141030 entity_schema = schema_name
10151031 ent = self ._get_entity_by_schema (entity_schema )
10161032 if not ent or not ent .get ("MetadataId" ):
1017- raise RuntimeError (f"Table '{ entity_schema } ' not found." )
1033+ raise MetadataError (
1034+ f"Table '{ entity_schema } ' not found." ,
1035+ subcode = ec .METADATA_TABLE_NOT_FOUND ,
1036+ )
10181037 metadata_id = ent ["MetadataId" ]
10191038 url = f"{ self .api } /EntityDefinitions({ metadata_id } )"
10201039 r = self ._request ("delete" , url )
@@ -1026,7 +1045,10 @@ def _create_table(self, tablename: str, schema: Dict[str, Any]) -> Dict[str, Any
10261045
10271046 ent = self ._get_entity_by_schema (entity_schema )
10281047 if ent :
1029- raise RuntimeError (f"Table '{ entity_schema } ' already exists. No update performed." )
1048+ raise MetadataError (
1049+ f"Table '{ entity_schema } ' already exists." ,
1050+ subcode = ec .METADATA_TABLE_ALREADY_EXISTS ,
1051+ )
10301052
10311053 created_cols : List [str ] = []
10321054 primary_attr_schema = "new_Name" if "_" not in entity_schema else f"{ entity_schema .split ('_' ,1 )[0 ]} _Name"
@@ -1080,7 +1102,10 @@ def _flush_cache(
10801102 """
10811103 k = (kind or "" ).strip ().lower ()
10821104 if k != "picklist" :
1083- raise ValueError (f"Unsupported cache kind '{ kind } ' (only 'picklist' is implemented)" )
1105+ raise ValidationError (
1106+ f"Unsupported cache kind '{ kind } ' (only 'picklist' is implemented)" ,
1107+ subcode = ec .VALIDATION_UNSUPPORTED_CACHE_KIND ,
1108+ )
10841109
10851110 removed = len (self ._picklist_label_cache )
10861111 self ._picklist_label_cache .clear ()
0 commit comments