88from dataclasses import dataclass
99from datetime import datetime , timedelta , timezone
1010from enum import Enum
11+ from importlib import import_module
12+ from importlib .util import find_spec
1113from typing import Any
1214
15+ import requests
1316from requests .adapters import HTTPAdapter , Retry
1417from requests .exceptions import RequestException
1518from requests_pkcs12 import Pkcs12Adapter
16- from xsdata .exceptions import ParserError
17- from xsdata .formats .dataclass .client import Client , ClientValueError , Config
18- from xsdata .formats .dataclass .parsers import DictDecoder
19+
20+ XSDATA_AVAILABLE = find_spec ("xsdata" ) is not None
21+
22+ if XSDATA_AVAILABLE :
23+ ParserError = import_module ("xsdata.exceptions" ).ParserError
24+ _client_mod = import_module ("xsdata.formats.dataclass.client" )
25+ _parser_mod = import_module ("xsdata.formats.dataclass.parsers" )
26+ Client = _client_mod .Client
27+ ClientValueError = _client_mod .ClientValueError
28+ Config = _client_mod .Config
29+ DictDecoder = _parser_mod .DictDecoder
30+ else :
31+
32+ class ParserError (Exception ):
33+ """Raised when parsing a SOAP response fails."""
34+
35+ class ClientValueError (ValueError ):
36+ """Raised when a payload does not match client expectations."""
37+
38+ class _ClientBase :
39+ """Fallback base class used when xsdata isn't installed."""
40+
41+ Client = _ClientBase
1942
2043_logger = logging .Logger (__name__ )
2144
@@ -136,14 +159,22 @@ def __init__(
136159 elif uf :
137160 self .uf = uf .value
138161
139- super ().__init__ (config = kwargs .get ("config" , {}), ** kwargs )
162+ self ._xsdata_available = XSDATA_AVAILABLE
163+ if self ._xsdata_available :
164+ super ().__init__ (config = kwargs .get ("config" , {}), ** kwargs )
165+ else :
166+ self .session = requests .Session ()
140167 self .versao = versao
141168 self .pkcs12_data = pkcs12_data
142169 self .pkcs12_password = pkcs12_password
143170 self .verify_ssl = verify_ssl
144171 self .service = service
145- self .transport .timeout = timeout
146- self .transport .session .verify = self .verify_ssl
172+ if self ._xsdata_available :
173+ self .transport .timeout = timeout
174+ self .transport .session .verify = self .verify_ssl
175+ else :
176+ self .timeout = timeout
177+ self .session .verify = self .verify_ssl
147178 self .fake_certificate = fake_certificate
148179 self .soap12_envelope = soap12_envelope
149180 self .wrap_response = wrap_response
@@ -190,16 +221,17 @@ def send(
190221 The response model instance.
191222 """
192223 server = "https://" + location .split ("/" )[2 ]
193- self .config = Config .from_service (action_class , location = location )
224+ if self ._xsdata_available :
225+ self .config = Config .from_service (action_class , location = location )
194226
195227 retries = Retry ( # retry in case of errors
196228 total = RETRIES ,
197229 backoff_factor = BACKOFF_FACTOR ,
198230 status_forcelist = RETRY_ERRORS ,
199231 )
200- self .transport . session .mount (server , HTTPAdapter (max_retries = retries ))
232+ self ._session .mount (server , HTTPAdapter (max_retries = retries ))
201233 if not self .fake_certificate :
202- self .transport . session .mount (
234+ self ._session .mount (
203235 server ,
204236 Pkcs12Adapter (
205237 pkcs12_data = self .pkcs12_data ,
@@ -215,9 +247,7 @@ def send(
215247 try :
216248 _logger .debug (f"Sending SOAP request to { location } with headers: { headers } " )
217249 _logger .debug (f"SOAP request payload: { data } " )
218- original_response = self .transport .post (
219- location , data = data , headers = headers
220- )
250+ original_response = self ._post (location , data = data , headers = headers )
221251 response = original_response .decode ()
222252 _logger .debug (f"SOAP response: { response } " )
223253
@@ -244,12 +274,15 @@ def send(
244274 )
245275 response = response .replace (SOAP12_ENV_NS , SOAP11_ENV_NS )
246276
247- res = self .parser .from_string (response , action_class .output )
277+ if self ._xsdata_available :
278+ res = self .parser .from_string (response , action_class .output )
279+ else :
280+ res = response
248281 if not self .wrap_response :
249282 return res
250283
251284 return WrappedResponse (
252- webservice = action_class . soapAction . split ( "/" )[ - 1 ] ,
285+ webservice = self . _webservice_name ( action_class ) ,
253286 envio_raiz = placeholder_content ,
254287 envio_xml = data .encode (),
255288 resposta = res ,
@@ -259,13 +292,51 @@ def send(
259292 _logger .error (f"Failed to send SOAP request to { location } : { e } " )
260293 raise
261294 except ParserError as e :
295+ if not self ._xsdata_available :
296+ raise
262297 _logger .error (
263298 f"Failed to parse SOAP response as { action_class .output } \n "
264299 f"SOAP response:\n { response } "
265300 )
266301 _logger .error (f"Error: { e } " )
267302 raise
268303
304+ def prepare_headers (self , headers : dict ) -> dict :
305+ """Prepare request headers.
306+
307+ Keep xsdata behavior when available and default to plain HTTP headers
308+ when running in lightweight mode without xsdata.
309+ """
310+ if self ._xsdata_available :
311+ return super ().prepare_headers (headers )
312+
313+ return {
314+ "Content-Type" : "text/xml; charset=utf-8" ,
315+ ** headers ,
316+ }
317+
318+ @property
319+ def _session (self ):
320+ return self .transport .session if self ._xsdata_available else self .session
321+
322+ def _post (self , location : str , data : str , headers : dict ) -> bytes :
323+ if self ._xsdata_available :
324+ return self .transport .post (location , data = data , headers = headers )
325+
326+ response = self .session .post (
327+ location ,
328+ data = data ,
329+ headers = headers ,
330+ timeout = self .timeout ,
331+ )
332+ response .raise_for_status ()
333+ return response .content
334+
335+ @staticmethod
336+ def _webservice_name (action_class : Any ) -> str :
337+ soap_action = getattr (action_class , "soapAction" , "" )
338+ return soap_action .split ("/" )[- 1 ] if soap_action else "unknown"
339+
269340 def prepare_payload (
270341 self ,
271342 obj : Any ,
@@ -291,6 +362,18 @@ def prepare_payload(
291362 Raises:
292363 ClientValueError: If the config input type doesn't match the given object.
293364 """
365+ if not self ._xsdata_available :
366+ if isinstance (obj , bytes ):
367+ return obj .decode ()
368+ if isinstance (obj , str ):
369+ return obj
370+ if isinstance (obj , dict ) and "raw_xml" in obj :
371+ return str (obj ["raw_xml" ])
372+ raise ClientValueError (
373+ "xsdata is not installed: pass the SOAP payload as `str`, `bytes` "
374+ "or a dict containing a `raw_xml` key."
375+ )
376+
294377 if isinstance (obj , dict ):
295378 decoder = DictDecoder (context = self .serializer .context )
296379 obj = decoder .decode (obj , self .config .input )
0 commit comments