1010
1111from .http import HttpClient
1212from .odata_upload_files import ODataFileUpload
13- from .errors import HttpError
13+ from .errors import HttpError , ValidationError , MetadataError , SQLParseError
1414from . import error_codes as ec
1515
1616
@@ -76,45 +76,67 @@ 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" ),
102+ pass
103+ sc = r .status_code
104+ sub_map = {
105+ 400 : ec .HTTP_400 ,
106+ 401 : ec .HTTP_401 ,
107+ 403 : ec .HTTP_403 ,
108+ 404 : ec .HTTP_404 ,
109+ 409 : ec .HTTP_409 ,
110+ 412 : ec .HTTP_412 ,
111+ 415 : ec .HTTP_415 ,
112+ 429 : ec .HTTP_429 ,
113+ 500 : ec .HTTP_500 ,
114+ 502 : ec .HTTP_502 ,
115+ 503 : ec .HTTP_503 ,
116+ 504 : ec .HTTP_504 ,
107117 }
118+ subcode = sub_map .get (sc , f"http_{ sc } " )
119+ correlation_id = headers .get ("x-ms-correlation-request-id" ) or headers .get ("x-ms-correlation-id" )
120+ request_id = headers .get ("x-ms-client-request-id" ) or headers .get ("request-id" ) or headers .get ("x-ms-request-id" )
121+ traceparent = headers .get ("traceparent" )
108122 ra = headers .get ("Retry-After" )
123+ retry_after = None
109124 if ra :
110- details ["retry_after" ] = ra
111- is_transient = r .status_code in (429 , 502 , 503 , 504 )
125+ try :
126+ retry_after = int (ra )
127+ except Exception :
128+ retry_after = None
129+ is_transient = sc in (429 , 502 , 503 , 504 )
112130 raise HttpError (
113- message ,
131+ msg ,
132+ status_code = sc ,
114133 subcode = subcode ,
115- status_code = r .status_code ,
116- details = details ,
117- source = {"method" : method , "url" : url },
134+ service_error_code = svc_code ,
135+ correlation_id = correlation_id ,
136+ request_id = request_id ,
137+ traceparent = traceparent ,
138+ body_excerpt = body_excerpt ,
139+ retry_after = retry_after ,
118140 is_transient = is_transient ,
119141 )
120142
@@ -497,8 +519,10 @@ def _query_sql(self, sql: str) -> list[dict[str, Any]]:
497519 RuntimeError
498520 If metadata lookup for the logical name fails.
499521 """
500- if not isinstance (sql , str ) or not sql .strip ():
501- raise ValueError ("sql must be a non-empty string" )
522+ if not isinstance (sql , str ):
523+ raise ValidationError ("sql must be a string" , subcode = ec .VALIDATION_SQL_NOT_STRING )
524+ if not sql .strip ():
525+ raise ValidationError ("sql must be a non-empty string" , subcode = ec .VALIDATION_SQL_EMPTY )
502526 sql = sql .strip ()
503527
504528 # Extract logical table name via helper (robust to identifiers ending with 'from')
@@ -567,11 +591,17 @@ def _entity_set_from_logical(self, logical: str) -> str:
567591 items = []
568592 if not items :
569593 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 ""
570- raise RuntimeError (f"Unable to resolve entity set for logical name '{ logical } '. Provide the singular logical name.{ plural_hint } " )
594+ raise MetadataError (
595+ f"Unable to resolve entity set for logical name '{ logical } '. Provide the singular logical name.{ plural_hint } " ,
596+ subcode = ec .METADATA_ENTITYSET_NOT_FOUND ,
597+ )
571598 md = items [0 ]
572599 es = md .get ("EntitySetName" )
573600 if not es :
574- raise RuntimeError (f"Metadata response missing EntitySetName for logical '{ logical } '." )
601+ raise MetadataError (
602+ f"Metadata response missing EntitySetName for logical '{ logical } '." ,
603+ subcode = ec .METADATA_ENTITYSET_NAME_MISSING ,
604+ )
575605 self ._logical_to_entityset_cache [logical ] = es
576606 primary_id_attr = md .get ("PrimaryIdAttribute" )
577607 if isinstance (primary_id_attr , str ) and primary_id_attr :
@@ -1011,7 +1041,10 @@ def _delete_table(self, tablename: str) -> None:
10111041 entity_schema = schema_name
10121042 ent = self ._get_entity_by_schema (entity_schema )
10131043 if not ent or not ent .get ("MetadataId" ):
1014- raise RuntimeError (f"Table '{ entity_schema } ' not found." )
1044+ raise MetadataError (
1045+ f"Table '{ entity_schema } ' not found." ,
1046+ subcode = ec .METADATA_TABLE_NOT_FOUND ,
1047+ )
10151048 metadata_id = ent ["MetadataId" ]
10161049 url = f"{ self .api } /EntityDefinitions({ metadata_id } )"
10171050 r = self ._request ("delete" , url )
@@ -1023,7 +1056,10 @@ def _create_table(self, tablename: str, schema: Dict[str, Any]) -> Dict[str, Any
10231056
10241057 ent = self ._get_entity_by_schema (entity_schema )
10251058 if ent :
1026- raise RuntimeError (f"Table '{ entity_schema } ' already exists. No update performed." )
1059+ raise MetadataError (
1060+ f"Table '{ entity_schema } ' already exists." ,
1061+ subcode = ec .METADATA_TABLE_ALREADY_EXISTS ,
1062+ )
10271063
10281064 created_cols : List [str ] = []
10291065 primary_attr_schema = "new_Name" if "_" not in entity_schema else f"{ entity_schema .split ('_' ,1 )[0 ]} _Name"
@@ -1077,7 +1113,10 @@ def _flush_cache(
10771113 """
10781114 k = (kind or "" ).strip ().lower ()
10791115 if k != "picklist" :
1080- raise ValueError (f"Unsupported cache kind '{ kind } ' (only 'picklist' is implemented)" )
1116+ raise ValidationError (
1117+ f"Unsupported cache kind '{ kind } ' (only 'picklist' is implemented)" ,
1118+ subcode = ec .VALIDATION_UNSUPPORTED_CACHE_KIND ,
1119+ )
10811120
10821121 removed = len (self ._picklist_label_cache )
10831122 self ._picklist_label_cache .clear ()
0 commit comments