66from sap_cloud_sdk .dms ._auth import Auth
77from sap_cloud_sdk .dms .exceptions import (
88 DMSError ,
9+ DMSConflictException ,
910 DMSConnectionError ,
1011 DMSInvalidArgumentException ,
1112 DMSObjectNotFoundException ,
@@ -38,15 +39,19 @@ def get(
3839 tenant_subdomain : Optional [str ] = None ,
3940 headers : Optional [dict [str , str ]] = None ,
4041 user_claim : Optional [UserClaim ] = None ,
42+ params : Optional [dict [str , str ]] = None ,
4143 ) -> Response :
4244 logger .debug ("GET %s" , path )
43- return self ._handle (self ._execute (
44- lambda : requests .get (
45- f"{ self ._base_url } { path } " ,
46- headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
47- timeout = (self ._connect_timeout , self ._read_timeout ),
45+ return self ._handle (
46+ self ._execute (
47+ lambda : requests .get (
48+ f"{ self ._base_url } { path } " ,
49+ headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
50+ params = params ,
51+ timeout = (self ._connect_timeout , self ._read_timeout ),
52+ )
4853 )
49- ))
54+ )
5055
5156 def post (
5257 self ,
@@ -57,14 +62,16 @@ def post(
5762 user_claim : Optional [UserClaim ] = None ,
5863 ) -> Response :
5964 logger .debug ("POST %s" , path )
60- return self ._handle (self ._execute (
61- lambda : requests .post (
62- f"{ self ._base_url } { path } " ,
63- headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
64- json = payload ,
65- timeout = (self ._connect_timeout , self ._read_timeout ),
65+ return self ._handle (
66+ self ._execute (
67+ lambda : requests .post (
68+ f"{ self ._base_url } { path } " ,
69+ headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
70+ json = payload ,
71+ timeout = (self ._connect_timeout , self ._read_timeout ),
72+ )
6673 )
67- ))
74+ )
6875
6976 def put (
7077 self ,
@@ -75,14 +82,16 @@ def put(
7582 user_claim : Optional [UserClaim ] = None ,
7683 ) -> Response :
7784 logger .debug ("PUT %s" , path )
78- return self ._handle (self ._execute (
79- lambda : requests .put (
80- f"{ self ._base_url } { path } " ,
81- headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
82- json = payload ,
83- timeout = (self ._connect_timeout , self ._read_timeout ),
85+ return self ._handle (
86+ self ._execute (
87+ lambda : requests .put (
88+ f"{ self ._base_url } { path } " ,
89+ headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
90+ json = payload ,
91+ timeout = (self ._connect_timeout , self ._read_timeout ),
92+ )
8493 )
85- ))
94+ )
8695
8796 def delete (
8897 self ,
@@ -92,13 +101,68 @@ def delete(
92101 user_claim : Optional [UserClaim ] = None ,
93102 ) -> Response :
94103 logger .debug ("DELETE %s" , path )
95- return self ._handle (self ._execute (
96- lambda : requests .delete (
97- f"{ self ._base_url } { path } " ,
98- headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
99- timeout = (self ._connect_timeout , self ._read_timeout ),
104+ return self ._handle (
105+ self ._execute (
106+ lambda : requests .delete (
107+ f"{ self ._base_url } { path } " ,
108+ headers = self ._merged_headers (tenant_subdomain , headers , user_claim ),
109+ timeout = (self ._connect_timeout , self ._read_timeout ),
110+ )
111+ )
112+ )
113+
114+ def post_form (
115+ self ,
116+ path : str ,
117+ * ,
118+ data : dict [str , str ],
119+ files : Optional [dict [str , Any ]] = None ,
120+ tenant_subdomain : Optional [str ] = None ,
121+ user_claim : Optional [UserClaim ] = None ,
122+ ) -> Response :
123+ """POST with form-encoded data and optional multipart file uploads.
124+
125+ Does not set Content-Type — ``requests`` sets it automatically
126+ to ``application/x-www-form-urlencoded`` or ``multipart/form-data``.
127+ """
128+ logger .debug ("POST_FORM %s" , path )
129+ return self ._handle (
130+ self ._execute (
131+ lambda : requests .post (
132+ f"{ self ._base_url } { path } " ,
133+ headers = self ._auth_header (tenant_subdomain , user_claim ),
134+ data = data ,
135+ files = files ,
136+ timeout = (self ._connect_timeout , self ._read_timeout ),
137+ )
138+ )
139+ )
140+
141+ def get_stream (
142+ self ,
143+ path : str ,
144+ * ,
145+ params : Optional [dict [str , str ]] = None ,
146+ tenant_subdomain : Optional [str ] = None ,
147+ user_claim : Optional [UserClaim ] = None ,
148+ ) -> Response :
149+ """GET that returns a raw streaming Response for binary content.
150+
151+ The caller is responsible for closing the response.
152+ On non-2xx status the usual typed exception is raised.
153+ """
154+ logger .debug ("GET_STREAM %s" , path )
155+ return self ._handle (
156+ self ._execute (
157+ lambda : requests .get (
158+ f"{ self ._base_url } { path } " ,
159+ headers = self ._merged_headers (tenant_subdomain , None , user_claim ),
160+ params = params ,
161+ stream = True ,
162+ timeout = (self ._connect_timeout , self ._read_timeout ),
163+ )
100164 )
101- ))
165+ )
102166
103167 def _execute (self , fn : Any ) -> Response :
104168 """Execute an HTTP call, wrapping network errors into DMSConnectionError."""
@@ -114,7 +178,20 @@ def _execute(self, fn: Any) -> Response:
114178 logger .error ("Unexpected network error" )
115179 raise DMSConnectionError ("Unexpected network error" ) from e
116180
117- def _default_headers (self , tenant_subdomain : Optional [str ] = None ) -> dict [str , str ]:
181+ def _auth_header (
182+ self ,
183+ tenant_subdomain : Optional [str ] = None ,
184+ user_claim : Optional [UserClaim ] = None ,
185+ ) -> dict [str , str ]:
186+ """Auth-only headers (no Content-Type). Used by post_form."""
187+ return {
188+ "Authorization" : f"Bearer { self ._auth .get_token (tenant_subdomain )} " ,
189+ ** self ._user_claim_headers (user_claim ),
190+ }
191+
192+ def _default_headers (
193+ self , tenant_subdomain : Optional [str ] = None
194+ ) -> dict [str , str ]:
118195 return {
119196 "Authorization" : f"Bearer { self ._auth .get_token (tenant_subdomain )} " ,
120197 "Content-Type" : "application/json" ,
@@ -152,24 +229,49 @@ def _handle(self, response: Response) -> Response:
152229 error_content = response .text
153230 logger .warning ("Request failed with status %s" , response .status_code )
154231
232+ # Try to extract the server's error message from the JSON body
233+ try :
234+ body = response .json ()
235+ server_message = body .get ("message" , "" ) if isinstance (body , dict ) else ""
236+ except Exception :
237+ server_message = ""
238+
155239 match response .status_code :
156240 case 400 :
157241 raise DMSInvalidArgumentException (
158- "Request contains invalid or disallowed parameters" , 400 , error_content
242+ server_message
243+ or "Request contains invalid or disallowed parameters" ,
244+ 400 ,
245+ error_content ,
159246 )
160247 case 401 | 403 :
161248 raise DMSPermissionDeniedException (
162- "Access denied — invalid or expired token" , response .status_code , error_content
249+ server_message or "Access denied — invalid or expired token" ,
250+ response .status_code ,
251+ error_content ,
163252 )
164253 case 404 :
165254 raise DMSObjectNotFoundException (
166- "The requested resource was not found" , 404 , error_content
255+ server_message or "The requested resource was not found" ,
256+ 404 ,
257+ error_content ,
258+ )
259+ case 409 :
260+ raise DMSConflictException (
261+ server_message
262+ or "The request conflicts with the current state of the resource" ,
263+ 409 ,
264+ error_content ,
167265 )
168266 case 500 :
169267 raise DMSRuntimeException (
170- "The DMS service encountered an internal error" , 500 , error_content
268+ server_message or "The DMS service encountered an internal error" ,
269+ 500 ,
270+ error_content ,
171271 )
172272 case _:
173273 raise DMSError (
174- f"Unexpected response from DMS service : " + error_content , response .status_code , error_content
175- )
274+ f"Unexpected response from DMS service: { error_content } " ,
275+ response .status_code ,
276+ error_content ,
277+ )
0 commit comments