pumpwood_communication.microservice_abc.base

Module with base classes for requests on Pumpwood Server.

1"""Module with base classes for requests on Pumpwood Server."""
2from .base import PumpWoodMicroServiceBase
3
4
5__docformat__ = "google"
6__all__ = [
7    PumpWoodMicroServiceBase
8]
class PumpWoodMicroServiceBase:
  41class PumpWoodMicroServiceBase:
  42    """Base class for Pumpwood MicroService.
  43
  44    Environment variables can be used to set MicroService parameters.
  45    Correct spelling is ``PUMPWOOD_COMMUNICATION__*``; legacy typo
  46    ``PUMPWOOD_COMUNICATION__*`` is still supported as fallback:
  47    - **PUMPWOOD_COMMUNICATION__DEFAULT_TIMEOUT:** Default requests timeout
  48        in seconds.
  49    - **PUMPWOOD_COMMUNICATION__DEBUG:** If object will be initiated using
  50        debug parameter. It will have more verbosity and login at each
  51        request. Options 'TRUE', 'FALSE'.
  52    - **PUMPWOOD_COMMUNICATION__VERIFY_SSL:** If requests will validate SSL
  53        certificate.
  54    """
  55
  56    __base_header = {'Content-Type': 'application/json'}
  57    """Base header for the requests."""
  58
  59    @staticmethod
  60    def _adjust_server_url(server_url):
  61        """Remove tralling / if present on server URL."""
  62        if server_url is None:
  63            return None
  64        if server_url[-1] != '/':
  65            return server_url + '/'
  66        else:
  67            return server_url
  68
  69    def __init__(self, name: str = None, server_url: str = None,
  70                 username: str = None, password: str = None,
  71                 verify_ssl: bool = True, debug: bool = None,
  72                 default_timeout: int = None, **kwargs):
  73        """Create new PumpWoodMicroService object.
  74
  75        Creates a new microservice object. If just name is passed, object must
  76        be initiate after with init() method.
  77
  78        Args:
  79            name:
  80                Name of the microservice, helps when exceptions
  81                are raised.
  82            server_url:
  83                URL of the server that will be connected.
  84            username:
  85                Username that will be logged on.
  86            password:
  87                Variable to be converted to JSON and posted along
  88                with the request.
  89            verify_ssl:
  90                Set if microservice will verify SSL certificate.
  91            debug:
  92                If microservice will be used as debug mode. This will obrigate
  93                auth token refresh for each call.
  94            default_timeout:
  95                Default timeout for Pumpwood calls.
  96            **kwargs:
  97                Other parameters used for compatibility between versions.
  98
  99        Returns:
 100            PumpWoodMicroService: New PumpWoodMicroService object
 101
 102        Raises:
 103            No particular Raises.
 104        """
 105        # Create attributes to be set at init function
 106        self.name = None
 107        """Name of the microservice instance."""
 108        self.server_url = None
 109        """Pumpwood server URL."""
 110        self._default_timeout: int = None
 111        """Default timeout for Pumpwood requests."""
 112        self._debug: bool = None
 113        """Name of the microservice instance."""
 114        self._verify_ssl: bool = None
 115        """If microservice should check the certificate."""
 116        self._is_mfa_login: bool = None
 117        """Set if is MFA login."""
 118        self.__headers: dict = None
 119        """Headers to be used on the requests."""
 120        self.__user: dict = None
 121        """Information of the logged user."""
 122        self.__auth_header: dict = None
 123        """Authenticated auth header."""
 124        self.__token_expiry: pd.Timedelta = None
 125        """Expirity datetime of the authetication token."""
 126        self.__username: str = None
 127        """Username associated with microservice."""
 128        self.__password: str = None
 129        """Password associated with microservice."""
 130        self.init(
 131            name=name, server_url=server_url,
 132            username=username, password=password,
 133            verify_ssl=verify_ssl, debug=debug,
 134            default_timeout=default_timeout)
 135
 136    def init(self, name: str = None, server_url: str = None,
 137             username: str = None, password: str = None,
 138             verify_ssl: bool = True, debug: bool = None,
 139             default_timeout: int = None, **kwargs):
 140        """Lazzy initialization of the MicroService of object.
 141
 142        This function might be usefull to use the object as a singleton at
 143        the backends. Using this function it is possible to instanciate an
 144        empty object and them set the attributes latter at the systems.
 145
 146        Args:
 147            name:
 148                Name of the microservice, helps when exceptions
 149                are raised.
 150            server_url:
 151                URL of the server that will be connected.
 152            username:
 153                Username that will be logged on.
 154            password:
 155                Variable to be converted to JSON and posted along
 156                with the request.
 157            verify_ssl:
 158                Set if microservice will verify SSL certificate.
 159            debug:
 160                If microservice will be used as debug mode. This will obrigate
 161                auth token refresh for each call.
 162            default_timeout:
 163                Default timeout for Pumpwood calls.
 164            **kwargs:
 165                Other parameters used for compatibility between versions.
 166
 167        Returns:
 168            No return
 169
 170        Raises:
 171            No particular Raises
 172        """
 173        self.name = name
 174        """Name of the microservice instance."""
 175        self.server_url = self._adjust_server_url(server_url)
 176        """Pumpwood server URL."""
 177
 178        # Set parameter using arguments or environment variables
 179        if default_timeout is None:
 180            self._default_timeout = DEFAULT_TIMEOUT
 181        else:
 182            self._default_timeout = default_timeout
 183
 184        if debug is None:
 185            self._debug = DEBUG
 186        else:
 187            self._debug = debug
 188
 189        if verify_ssl is None:
 190            self._verify_ssl = VERIFY_SSL
 191        else:
 192            self._verify_ssl = verify_ssl
 193
 194        self._is_mfa_login = False
 195        self.__headers = None
 196        self.__user = None
 197        self.__auth_header = None
 198        self.__token_expiry = None
 199        self.__username = username
 200        self.__password = password
 201
 202    @staticmethod
 203    def angular_json(request_result) -> Any:
 204        r"""Convert text to Json removing any XSSI at the beging of JSON.
 205
 206        Some backends add `)]}',\n` at the beginning of the JSON data to
 207        prevent injection of functions. This function remove this characters
 208        if present.
 209
 210        Args:
 211            request_result:
 212                JSON request to be converted.
 213
 214        Returns:
 215            No return
 216
 217        Raises:
 218            PumpWoodJSONLoadError:
 219                If it is not possible to load JSON from request data.
 220        """
 221        if request_result.text == '':
 222            return None
 223
 224        string_start = ")]}',\n"
 225        try:
 226            if request_result.text[:6] == string_start:
 227                return (orjson.loads(request_result.text[6:]))
 228            else:
 229                return (orjson.loads(request_result.text))
 230        except Exception:
 231            msg = "Can not decode to Json"
 232            raise PumpWoodJSONLoadError(
 233                message=msg, payload={"request_data": request_result.text})
 234
 235    def time_to_expiry(self) -> pd.Timedelta:
 236        """Return time to token expiry.
 237
 238        Args:
 239            No Args.
 240
 241        Returns:
 242            Return time until token expiration.
 243        """
 244        if self.__token_expiry is None:
 245            return None
 246
 247        now_datetime = pd.to_datetime(
 248            datetime.datetime.now(datetime.UTC), utc=True)
 249        time_to_expiry = self.__token_expiry - now_datetime
 250        return time_to_expiry
 251
 252    def is_credential_set(self) -> bool:
 253        """Check if username and password are set on object.
 254
 255        Args:
 256            No Args.
 257
 258        Returns:
 259            True if usename and password were set during object creation or
 260            later with init function.
 261        """
 262        is_username_not_none = self.__username is not None
 263        is_password_not_none = self.__password is not None
 264        return is_username_not_none and is_password_not_none
 265
 266    @classmethod
 267    def is_invalid_token_response(cls, response: Response) -> bool:
 268        """Check if reponse has invalid token error.
 269
 270        Args:
 271            response:
 272                Request reponse to check for invalid token.
 273
 274        Returns:
 275            Return True if response has an invalid token status.
 276        """
 277        if response.status_code == 401:
 278            return True
 279        return False
 280
 281    def _login_resquest(self) -> Response:
 282        """Request login with object credentials.
 283
 284        Args:
 285            No Args
 286
 287        Returns (Response):
 288            Request object.
 289        """
 290        login_url = urljoin(
 291            self.server_url, 'rest/registration/login/')
 292
 293        # Make a retry loop for authentication
 294        login_result = None
 295        for i in range(5):
 296            login_result = requests.post(
 297                login_url, json={
 298                    'username': self.__username,
 299                    'password': self.__password},
 300                verify=self._verify_ssl, timeout=self._default_timeout)
 301            if login_result.ok:
 302                try:
 303                    login_data = self.angular_json(login_result)
 304                    return login_data
 305                except Exception: # NOQA
 306                    pass
 307
 308            # Handle Unauthorized responses
 309            elif self.is_invalid_token_response(login_result):
 310                error_data = self.angular_json(login_result)
 311                raise PumpWoodUnauthorized(
 312                    message="Login is not possible",
 313                    payload=error_data)
 314
 315            # Handle Forbidden responses
 316            elif login_result.status_code == 403:
 317                error_data = self.angular_json(login_result)
 318                raise PumpWoodForbidden(
 319                    message="Login resquest is forbidden",
 320                    payload=error_data)
 321            time.sleep(0.2)
 322
 323        # If response is not returned then something is not ok
 324        error_data = self.angular_json(login_result)
 325        raise PumpWoodOtherException(
 326            message="Login not possible, server is not responding correctly",
 327            payload=error_data)
 328
 329    def confirm_mfa_code(self, mfa_login_data: dict) -> dict:
 330        """Ask user to confirm MFA code to login.
 331
 332        Open an input interface at terminal for user to validate MFA token.
 333
 334        Args:
 335            mfa_login_data:
 336                Result from login request with 'mfa_token'
 337                as key.
 338
 339        Returns:
 340            Return login returned with MFA confimation.
 341
 342        Raise:
 343            Raise error if reponse is not valid using error_handler.
 344        """
 345        code = input("## Please enter MFA code: ")
 346        url = urljoin(
 347            self.server_url, 'rest/registration/mfa-validate-code/')
 348        mfa_response = requests.post(url, headers={
 349            "X-PUMPWOOD-MFA-Autorization": mfa_login_data['mfa_token']},
 350            json={"mfa_code": code}, timeout=self._default_timeout)
 351        self.error_handler(mfa_response)
 352
 353        # Set _is_mfa_login true to indicate that login required MFA
 354        self._is_mfa_login = True
 355        return self.angular_json(mfa_response)
 356
 357    def login(self, force_refresh: bool = False) -> None:
 358        """Log microservice in using username and password provided.
 359
 360        Args:
 361            force_refresh (bool):
 362                Force token refresh despise still valid
 363                according to self.__token_expiry.
 364
 365        Returns:
 366            No return
 367
 368        Raises:
 369            Exception:
 370                If login response has status diferent from 200.
 371        """
 372        if not self.is_credential_set():
 373            raise PumpWoodUnauthorized(
 374                message="Microservice username or/and password not set")
 375
 376        # Check if expiry time is 1h from now
 377        refresh_expiry = False
 378        if self.__token_expiry is None:
 379            refresh_expiry = True
 380        else:
 381            time_to_expiry = self.time_to_expiry()
 382            if time_to_expiry < datetime.timedelta(hours=1):
 383                refresh_expiry = True
 384
 385        # When if debug always refresh token
 386        if refresh_expiry or force_refresh or self._debug:
 387            login_data = self._login_resquest()
 388            if 'mfa_token' in login_data.keys():
 389                login_data = self.confirm_mfa_code(
 390                    mfa_login_data=login_data)
 391
 392            self.set_auth_header(
 393                auth_token='Token ' + login_data['token'],
 394                token_expiry=pd.to_datetime(login_data['expiry']),
 395                user=login_data["user"])
 396        else:
 397            # Token is not expired or envicted, them keep same token
 398            return None
 399
 400    def _evict_auth_state(self) -> None:
 401        """Clear local authentication state after logout.
 402
 403        Resets token expiry, auth header, and cached user information so
 404        ``is_superuser`` and ``login`` behave correctly after logout.
 405        """
 406        self.__token_expiry = None
 407        self.__auth_header = None
 408        self.__user = None
 409
 410    def logout(self, auth_header: dict = None) -> bool:
 411        """Logout token.
 412
 413        Args:
 414            auth_header:
 415                Authentication header.
 416
 417        Returns:
 418            True if logout was ok.
 419        """
 420        resp = self.request_post(
 421            url='rest/registration/logout/',
 422            data={}, auth_header=auth_header)
 423        self._evict_auth_state()
 424        return resp is None
 425
 426    def logout_all(self, auth_header: dict = None) -> bool:
 427        """Logout all tokens from user.
 428
 429        Args:
 430            auth_header (dict):
 431                Authentication header.
 432
 433        Returns:
 434            True if logout all was ok.
 435        """
 436        resp = self.request_post(
 437            url='rest/registration/logoutall/',
 438            data={}, auth_header=auth_header)
 439        self._evict_auth_state()
 440        return resp is None
 441
 442    def get_auth_header(self) -> dict:
 443        """Retrieve auth_header and token_expiry from object.
 444
 445        Args:
 446            No Args.
 447
 448        Returns:
 449            Return authorization header and token_expiry datetime from object.
 450        """
 451        # Copy the dictonary to avoid updating the original one
 452        return copy.deepcopy({
 453            "auth_header": self.__auth_header,
 454            "token_expiry": self.__token_expiry})
 455
 456    def set_auth_header(self, auth_token: str,
 457                        token_expiry: pd.Timestamp,
 458                        user: dict = None) -> dict:
 459        """Retrieve auth_header and token_expiry from object.
 460
 461        Args:
 462            auth_token (str):
 463                Auth token that will be set for authentication.
 464            token_expiry (pd.Timestamp):
 465                Token expiry time.
 466            user (dict):
 467                User information to be set on authetication.
 468
 469        Returns:
 470            Return authorization header and token_expiry datetime from object.
 471        """
 472        # Copy the dictonary to avoid updating the original one
 473        self.__auth_header = {
 474            'Authorization': auth_token}
 475        self.__token_expiry = token_expiry
 476        self.__user = user
 477        return True
 478    
 479    def is_superuser(self) -> bool:
 480        """Check if is superuser.
 481
 482        Args:
 483            No Args.
 484
 485        Returns:
 486            Return True if is superuser.
 487        """
 488        user = self.__user
 489        if user is None:
 490            return False
 491        return user.get("is_superuser", False)
 492    
 493    def _resolve_base_filter_skip(self, base_filter_skip):
 494        """Resolve base filter skip.
 495
 496        Args:
 497            base_filter_skip:
 498                Base filter skip to be resolved.
 499
 500        Returns:
 501            Return base filter skip resolved.
 502        """
 503        if base_filter_skip is None:
 504            if self.is_superuser():
 505                base_filter_skip = ['ALL']
 506            else:
 507                base_filter_skip = []
 508        return base_filter_skip
 509
 510    def _check_auth_header(self, auth_header: dict,
 511                           multipart: bool = False) -> dict:
 512        """Check if auth_header is set or auth_header if provided.
 513
 514        Args:
 515            auth_header (dict):
 516                AuthHeader to substitute the microservice original
 517                at the request (user impersonation).
 518            multipart (dict):
 519                Set if call should be made as a multipart instead of JSON.
 520
 521        Returns (dict):
 522            Return a header dict to be used in requests.
 523
 524        Raises:
 525            PumpWoodUnauthorized:
 526                If microservice is not logged and a auth_header method
 527                argument is not provided.
 528            PumpWoodUnauthorized:
 529                If microservice is logged and a auth_header method argument
 530                is provided.
 531        """
 532        if auth_header is None:
 533            # Login will refresh token if it is 1h to expire, it will also
 534            # check if credentials are set.
 535            self.login()
 536            auth_header_data = self.get_auth_header()
 537            auth_header = auth_header_data['auth_header']
 538            if multipart:
 539                return auth_header
 540            else:
 541                return self.__base_header | auth_header
 542        else:
 543            if self.is_credential_set():
 544                msg = (
 545                    'Microservice [{object_name}] with credentials and '
 546                    'auth_header was provided')
 547                raise PumpWoodUnauthorized(
 548                    message=msg, payload={'object_name': self.name})
 549
 550            # Set base header as JSON since unserialization is done using
 551            # Pumpwood Communication serialization function
 552            temp__auth_header = auth_header.copy()
 553            if multipart:
 554                return temp__auth_header
 555            else:
 556                return self.__base_header | temp__auth_header
 557
 558    @classmethod
 559    def error_handler(cls, response):
 560        """Handle request error.
 561
 562        Check if is a Json and propagate the error with
 563        same type if possible. If not Json raises the content.
 564
 565        Args:
 566            response:
 567                response to be handled, it is a PumpWoodException
 568                return it will raise the same exception at microservice
 569                object.
 570
 571        Returns:
 572            No return.
 573
 574        Raises:
 575            PumpWoodOtherException:
 576                If content-type is not application/json.
 577            PumpWoodOtherException:
 578                If content-type is application/json, but type not
 579                present or not recognisable at `exceptions.exceptions_dict`.
 580            Other PumpWoodException sub-types:
 581                If content-type is application/json if type is present and
 582                recognisable.
 583
 584        Example:
 585            No example
 586        """
 587        if not response.ok:
 588            utcnow = datetime.datetime.now(datetime.UTC)
 589            response_content_type = response.headers['content-type']
 590
 591            # Request information
 592            url = response.url
 593            method = response.request.method
 594            if 'application/json' not in response_content_type.lower():
 595                # Raise the exception as first in exception deep.
 596                exception_dict = [{
 597                    "exception_url": url,
 598                    "exception_method": method,
 599                    "exception_utcnow": utcnow.isoformat(),
 600                    "exception_deep": 1}]
 601                raise PumpWoodOtherException(
 602                    message=response.text, payload={
 603                        "!exception_stack!": exception_dict})
 604
 605            # Build error stack
 606            response_dict = cls.angular_json(response)
 607
 608            # Removing previous error stack
 609            payload = copy.deepcopy(
 610                response_dict.get("payload", {}))
 611            exception_stack = copy.deepcopy(
 612                payload.pop("!exception_stack!", []))
 613
 614            exception_deep = len(exception_stack)
 615            exception_dict = {
 616                "exception_url": url,
 617                "exception_method": method,
 618                "exception_utcnow": utcnow.isoformat(),
 619                "exception_deep": exception_deep + 1
 620            }
 621            exception_stack.insert(0, exception_dict)
 622            payload["!exception_stack!"] = exception_stack
 623
 624            ###################
 625            # Propagate error #
 626            # get exception using 'type' key at response data and get the
 627            # exception from exceptions_dict at exceptions
 628            exception_message = response_dict.get(
 629                "message_not_fmt", response_dict.get("message", ""))
 630            exception_type = response_dict.get("type")
 631            TempPumpwoodException = exceptions_dict\
 632                .get(exception_type, None)
 633            if TempPumpwoodException is not None:
 634                raise TempPumpwoodException(
 635                    message=exception_message,
 636                    status_code=response_dict.get(
 637                        "status_code", response.status_code),
 638                    payload=payload,
 639                    was_translated=response_dict.get("was_translated", False),
 640                    parallel=response_dict.get("parallel", False))
 641            else:
 642                # If token is invalid is at response, return a
 643                # PumpWoodUnauthorized error
 644                is_invalid_token = cls.is_invalid_token_response(response)
 645                response_dict["!exception_stack!"] = exception_stack
 646                if is_invalid_token:
 647                    raise PumpWoodUnauthorized(
 648                        message="Invalid token", payload=payload)
 649                else:
 650                    # If the error is not mapped return a
 651                    # PumpWoodOtherException limiting the message size to 1k
 652                    # characters
 653                    raise PumpWoodOtherException(
 654                        message="Not mapped exception JSON",
 655                        payload=response_dict)
 656
 657    def _request_post_json(self, post_url: str, data: any,
 658                           auth_header: dict = None,
 659                           parameters: None | dict = None) -> any:
 660        """Make a POST a request to url with data as JSON payload.
 661
 662        Args:
 663            post_url:
 664                URL to make the request, already with server url.
 665            data:
 666                Data to be used as Json payload.
 667            parameters:
 668                URL parameters.
 669            auth_header:
 670                AuthHeader to substitute the microservice original
 671                at the request (user impersonation).
 672
 673        Returns:
 674            Return the post response data.
 675
 676        Raises:
 677            PumpWoodException sub-types:
 678                Response is passed to error_handler.
 679        """
 680        parameters = {} if parameters is None else parameters
 681
 682        response = None
 683        request_header = self._check_auth_header(auth_header=auth_header)
 684        dumped_data = pumpJsonDump(data)
 685        response = requests.post(
 686            url=post_url, data=dumped_data,
 687            params=parameters, verify=self._verify_ssl,
 688            headers=request_header, timeout=self._default_timeout)
 689
 690        # Retry request if token is not valid forcing token renew
 691        retry_with_login = (
 692            self.is_invalid_token_response(response) and
 693            auth_header is None)
 694        if not retry_with_login:
 695            return response
 696        else:
 697            # Force token refresh if Unauthorized
 698            time.sleep(0.5)
 699            self.login(force_refresh=True)
 700            request_header = self._check_auth_header(auth_header=auth_header)
 701            return requests.post(
 702                url=post_url, data=dumped_data,
 703                params=parameters, verify=self._verify_ssl,
 704                headers=request_header, timeout=self._default_timeout)
 705
 706    def _request_post_multi(self, post_url: str, data: any, files: list = None,
 707                            auth_header: dict = None,
 708                            parameters: None | dict = None) -> any:
 709        """Make a POST a request to url with data as multipart payload.
 710
 711        Args:
 712            post_url:
 713                URL to make the request, already with server url.
 714            data:
 715                Data to be used as Json payload.
 716            files:
 717                A dictonary with file data, files will be set on field
 718                corresponding.to dictonary key.
 719                `{'file1': open('file1', 'rb'), {'file2': open('file2', 'rb')}`
 720            parameters:
 721                URL parameters.
 722            auth_header:
 723                AuthHeader to substitute the microservice original
 724                at the request (user impersonation).
 725
 726        Returns:
 727            Return the post response data.
 728
 729        Raises:
 730            PumpWoodException sub-types:
 731                Response is passed to error_handler.
 732        """
 733        parameters = {} if parameters is None else parameters
 734
 735        # Request with files are done using multipart serializing all fields
 736        # as JSON
 737        request_header = self._check_auth_header(
 738            auth_header=auth_header, multipart=True)
 739        temp_data = {'__json__': pumpJsonDump(data)}
 740
 741        response = requests.post(
 742            url=post_url, data=temp_data, files=files, params=parameters,
 743            verify=self._verify_ssl, headers=request_header,
 744            timeout=self._default_timeout)
 745        retry_with_login = (
 746            self.is_invalid_token_response(response) and
 747            auth_header is None)
 748        if not retry_with_login:
 749            return response
 750        else:
 751            # Force token refresh if Unauthorized
 752            time.sleep(0.5)
 753            self.login(force_refresh=True)
 754            request_header = self._check_auth_header(
 755                auth_header=auth_header, multipart=True)
 756            return requests.post(
 757                url=post_url, data=temp_data, files=files,
 758                params=parameters, verify=self._verify_ssl,
 759                headers=request_header, timeout=self._default_timeout)
 760
 761    @classmethod
 762    def _treat_response_for_file(cls, response: Response) -> dict:
 763        """Return if response contain a file.
 764
 765        Args:
 766            response (Response):
 767                Response to be checked for a file content.
 768
 769        Returns (bool):
 770            Returns if reponse has a file.
 771        """
 772        headers = response.headers
 773        content_disposition = headers.get('content-disposition')
 774        if content_disposition is None:
 775            return cls.angular_json(response)
 776        else:
 777            fname = re.findall("filename=(.+)", content_disposition)[0]
 778            return {
 779                "__file__": True,
 780                "content": response.content,
 781                "content-type": response.headers['content-type'],
 782                "filename": fname}
 783
 784    @classmethod
 785    def _dump_query_parameters(cls, parameters: dict) -> dict:
 786        """Dump query parameters to javascript compatibility.
 787
 788        Args:
 789            parameters (dict):
 790                Parameters to be parsed to JSON.
 791
 792        Returns:
 793            pass
 794        """
 795        # If parameters are not none convert them to json before
 796        # sending information on query string, 'True' is 'true' on javascript
 797        # for example
 798        if parameters is not None:
 799            temp_parameters = copy.deepcopy(parameters)
 800            for key in temp_parameters.keys():
 801                # Do not convert str to json, it put extra "" araound string
 802                if type(temp_parameters[key]) is not str:
 803                    temp_parameters[key] = pumpJsonDump(parameters[key])
 804            return temp_parameters
 805        else:
 806            return None
 807
 808    def request_post(self, url: str, data: any, files: list = None,
 809                     auth_header: dict = None,
 810                     parameters: None | dict = {}) -> any:
 811        """Make a POST a request to url with data as multipart/json payload.
 812
 813        Args:
 814            url:
 815                URL to make the request, already with server url.
 816            data:
 817                Data to be used as Json payload.
 818            files:
 819                A dictonary with file data, files will be set on field
 820                corresponding.to dictonary key.
 821                `{'file1': open('file1', 'rb'), {'file2': open('file2', 'rb')}`
 822            parameters:
 823                URL parameters.
 824            auth_header:
 825                AuthHeader to substitute the microservice original
 826                at the request (user impersonation).
 827
 828        Returns:
 829            Return the post response data.
 830
 831        Raises:
 832            PumpWoodException sub-types:
 833                Response is passed to error_handler.
 834        """
 835        parameters = {} if parameters is None else parameters
 836
 837        post_url = urljoin(self.server_url, url)
 838        dumped_parameters = self._dump_query_parameters(parameters=parameters)
 839        response = None
 840        if files is None:
 841            response = self._request_post_json(
 842                post_url=post_url, data=data, auth_header=auth_header,
 843                parameters=dumped_parameters)
 844        else:
 845            response = self._request_post_multi(
 846                post_url=post_url, data=data, files=files,
 847                auth_header=auth_header, parameters=dumped_parameters)
 848
 849        # Handle errors and re-raise if Pumpwood Exceptions
 850        self.error_handler(response)
 851        return self._treat_response_for_file(response=response)
 852
 853    def request_get(self, url: str, parameters: None | dict = None,
 854                    auth_header: dict = None,
 855                    use_disk_cache: bool = False,
 856                    disk_cache_expire: int = None,
 857                    disk_cache_tag_dict: dict = None) -> Any:
 858        """Make a GET a request to url with data as JSON payload.
 859
 860        Add the auth_header acording to login information and refresh token
 861        if auth_header=None and object token is expired.
 862
 863        Args:
 864            url (str):
 865                URL to make the request.
 866            parameters (dict):
 867                URL parameters to make the request.
 868            auth_header (dict):
 869                Auth header to substitute the microservice original
 870                at the request (user impersonation).
 871            use_disk_cache (bool):
 872                If set true, get request will use local cache to reduce
 873                the requests to the backend.
 874            disk_cache_expire (int):
 875                Time in seconds to expire the cache, it None it will
 876                use de default set be PumpwoodCache.
 877            disk_cache_tag_dict (dict):
 878                Dictionary to be used as a tag on get request.
 879
 880        Returns:
 881            Return the post reponse data.
 882
 883        Raises:
 884            PumpWoodException sub-types:
 885                Raise exception if reponse is not 2XX and if 'type' key on
 886                JSON payload if found at exceptions_dict. Use the same
 887                exception, message and payload.
 888            PumpWoodOtherException:
 889                If exception type is not found or return is not a json.
 890        """
 891        parameters = {} if parameters is None else parameters
 892
 893        request_header = self._check_auth_header(auth_header)
 894        # If is set to use diskcache, it will create a hash cash using
 895        # the query paramerers, url and user access token. The
 896        # hash will be used as index, not exposing the token at cache
 897        # database
 898        hash_dict = None
 899        if use_disk_cache:
 900            hash_dict = RequestGetCacheHash(
 901                context='pumpwood_communication-request_get',
 902                authorization=request_header['Authorization'],
 903                parameters=parameters,
 904                url=url)
 905
 906            cache_results = default_cache.get(hash_dict=hash_dict)
 907            if cache_results is not None:
 908                msg = "get from cache url[{url}]".format(url=url)
 909                logger.info(msg)
 910                return cache_results
 911
 912        dumped_parameters = self._dump_query_parameters(parameters=parameters)
 913        get_url = urljoin(self.server_url, url)
 914        response = requests.get(
 915            get_url, verify=self._verify_ssl, headers=request_header,
 916            params=dumped_parameters, timeout=self._default_timeout)
 917
 918        # If token is expired, refresh it
 919        retry_with_login = (
 920            self.is_invalid_token_response(response) and
 921            auth_header is None)
 922        if retry_with_login:
 923            time.sleep(0.5)
 924            self.login(force_refresh=True)
 925            request_header = self._check_auth_header(auth_header=auth_header)
 926            response = requests.get(
 927                get_url, verify=self._verify_ssl, headers=request_header,
 928                params=dumped_parameters, timeout=self._default_timeout)
 929
 930        # Re-raise Pumpwood exceptions
 931        self.error_handler(response=response)
 932        results = self._treat_response_for_file(response=response)
 933
 934        # If is set to use cache for this calls, set the local cache
 935        if use_disk_cache and not results.get('__file__', False):
 936            default_cache.set(
 937                hash_dict=hash_dict, value=results,
 938                expire=disk_cache_expire,
 939                tag_dict=disk_cache_tag_dict)
 940        return results
 941
 942    def request_delete(self, url, parameters: dict = None,
 943                       auth_header: dict = None):
 944        """Make a DELETE a request to url with data as Json payload.
 945
 946        Args:
 947            url:
 948                Url to make the request.
 949            parameters:
 950                Dictionary with Urls parameters.
 951            auth_header:
 952                Auth header to substitute the microservice original
 953                at the request (user impersonation).
 954
 955        Returns:
 956            Return the delete reponse payload.
 957
 958        Raises:
 959            PumpWoodException sub-types:
 960                Raise exception if reponse is not 2XX and if 'type' key on
 961                JSON payload if found at exceptions_dict. Use the same
 962                exception, message and payload.
 963            PumpWoodOtherException:
 964                If exception type is not found or return is not a json.
 965        """
 966        request_header = self._check_auth_header(auth_header)
 967        dumped_parameters = self._dump_query_parameters(parameters=parameters)
 968
 969        post_url = self.server_url + url
 970        response = requests.delete(
 971            post_url, verify=self._verify_ssl, headers=request_header,
 972            params=dumped_parameters, timeout=self._default_timeout)
 973
 974        # Retry request if token is not valid forcing token renew
 975        retry_with_login = (
 976            self.is_invalid_token_response(response) and
 977            auth_header is None)
 978        if retry_with_login:
 979            time.sleep(0.5)
 980            self.login(force_refresh=True)
 981            request_header = self._check_auth_header(auth_header=auth_header)
 982            response = requests.delete(
 983                post_url, verify=self._verify_ssl, headers=request_header,
 984                params=dumped_parameters, timeout=self._default_timeout)
 985
 986        # Re-raise Pumpwood Exceptions
 987        self.error_handler(response)
 988        return self.angular_json(response)
 989
 990    def get_user_info(self, auth_header: dict = None,
 991                      use_disk_cache: bool = False,
 992                      disk_cache_expire: int = None) -> dict:
 993        """Get user info.
 994
 995        Args:
 996            auth_header (dict): = None
 997                AuthHeader to substitute the microservice original at
 998                request. If not passed, microservice object auth_header
 999                will be used.
1000            use_disk_cache (bool):
1001                It possible use disk cache.
1002            disk_cache_expire (int):
1003                Set a time to expire the cache. If not passed env variable
1004                ``PUMPWOOD_COMMUNICATION__AUTHORIZATION_CACHE_TIMEOUT``
1005                env variable (legacy spelling supported), default 60 s.
1006
1007        Returns:
1008            A serialized user object with information of the logged user.
1009        """
1010        url = "rest/registration/retrieveauthenticateduser/"
1011        temp_disk_cache_expire = (
1012            disk_cache_expire
1013            if disk_cache_expire is not None else
1014            AUTHORIZATION_CACHE_TIMEOUT)
1015
1016        user_info = self.request_get(
1017            url=url, auth_header=auth_header, use_disk_cache=use_disk_cache,
1018            disk_cache_expire=temp_disk_cache_expire)
1019        return user_info

Base class for Pumpwood MicroService.

Environment variables can be used to set MicroService parameters. Correct spelling is PUMPWOOD_COMMUNICATION__*; legacy typo PUMPWOOD_COMUNICATION__* is still supported as fallback:

  • PUMPWOOD_COMMUNICATION__DEFAULT_TIMEOUT: Default requests timeout in seconds.
  • PUMPWOOD_COMMUNICATION__DEBUG: If object will be initiated using debug parameter. It will have more verbosity and login at each request. Options 'TRUE', 'FALSE'.
  • PUMPWOOD_COMMUNICATION__VERIFY_SSL: If requests will validate SSL certificate.
PumpWoodMicroServiceBase( name: str = None, server_url: str = None, username: str = None, password: str = None, verify_ssl: bool = True, debug: bool = None, default_timeout: int = None, **kwargs)
 69    def __init__(self, name: str = None, server_url: str = None,
 70                 username: str = None, password: str = None,
 71                 verify_ssl: bool = True, debug: bool = None,
 72                 default_timeout: int = None, **kwargs):
 73        """Create new PumpWoodMicroService object.
 74
 75        Creates a new microservice object. If just name is passed, object must
 76        be initiate after with init() method.
 77
 78        Args:
 79            name:
 80                Name of the microservice, helps when exceptions
 81                are raised.
 82            server_url:
 83                URL of the server that will be connected.
 84            username:
 85                Username that will be logged on.
 86            password:
 87                Variable to be converted to JSON and posted along
 88                with the request.
 89            verify_ssl:
 90                Set if microservice will verify SSL certificate.
 91            debug:
 92                If microservice will be used as debug mode. This will obrigate
 93                auth token refresh for each call.
 94            default_timeout:
 95                Default timeout for Pumpwood calls.
 96            **kwargs:
 97                Other parameters used for compatibility between versions.
 98
 99        Returns:
100            PumpWoodMicroService: New PumpWoodMicroService object
101
102        Raises:
103            No particular Raises.
104        """
105        # Create attributes to be set at init function
106        self.name = None
107        """Name of the microservice instance."""
108        self.server_url = None
109        """Pumpwood server URL."""
110        self._default_timeout: int = None
111        """Default timeout for Pumpwood requests."""
112        self._debug: bool = None
113        """Name of the microservice instance."""
114        self._verify_ssl: bool = None
115        """If microservice should check the certificate."""
116        self._is_mfa_login: bool = None
117        """Set if is MFA login."""
118        self.__headers: dict = None
119        """Headers to be used on the requests."""
120        self.__user: dict = None
121        """Information of the logged user."""
122        self.__auth_header: dict = None
123        """Authenticated auth header."""
124        self.__token_expiry: pd.Timedelta = None
125        """Expirity datetime of the authetication token."""
126        self.__username: str = None
127        """Username associated with microservice."""
128        self.__password: str = None
129        """Password associated with microservice."""
130        self.init(
131            name=name, server_url=server_url,
132            username=username, password=password,
133            verify_ssl=verify_ssl, debug=debug,
134            default_timeout=default_timeout)

Create new PumpWoodMicroService object.

Creates a new microservice object. If just name is passed, object must be initiate after with init() method.

Arguments:
  • name: Name of the microservice, helps when exceptions are raised.
  • server_url: URL of the server that will be connected.
  • username: Username that will be logged on.
  • password: Variable to be converted to JSON and posted along with the request.
  • verify_ssl: Set if microservice will verify SSL certificate.
  • debug: If microservice will be used as debug mode. This will obrigate auth token refresh for each call.
  • default_timeout: Default timeout for Pumpwood calls.
  • **kwargs: Other parameters used for compatibility between versions.
Returns:

PumpWoodMicroService: New PumpWoodMicroService object

Raises:
  • No particular Raises.
name

Name of the microservice instance.

server_url

Pumpwood server URL.

def init( self, name: str = None, server_url: str = None, username: str = None, password: str = None, verify_ssl: bool = True, debug: bool = None, default_timeout: int = None, **kwargs):
136    def init(self, name: str = None, server_url: str = None,
137             username: str = None, password: str = None,
138             verify_ssl: bool = True, debug: bool = None,
139             default_timeout: int = None, **kwargs):
140        """Lazzy initialization of the MicroService of object.
141
142        This function might be usefull to use the object as a singleton at
143        the backends. Using this function it is possible to instanciate an
144        empty object and them set the attributes latter at the systems.
145
146        Args:
147            name:
148                Name of the microservice, helps when exceptions
149                are raised.
150            server_url:
151                URL of the server that will be connected.
152            username:
153                Username that will be logged on.
154            password:
155                Variable to be converted to JSON and posted along
156                with the request.
157            verify_ssl:
158                Set if microservice will verify SSL certificate.
159            debug:
160                If microservice will be used as debug mode. This will obrigate
161                auth token refresh for each call.
162            default_timeout:
163                Default timeout for Pumpwood calls.
164            **kwargs:
165                Other parameters used for compatibility between versions.
166
167        Returns:
168            No return
169
170        Raises:
171            No particular Raises
172        """
173        self.name = name
174        """Name of the microservice instance."""
175        self.server_url = self._adjust_server_url(server_url)
176        """Pumpwood server URL."""
177
178        # Set parameter using arguments or environment variables
179        if default_timeout is None:
180            self._default_timeout = DEFAULT_TIMEOUT
181        else:
182            self._default_timeout = default_timeout
183
184        if debug is None:
185            self._debug = DEBUG
186        else:
187            self._debug = debug
188
189        if verify_ssl is None:
190            self._verify_ssl = VERIFY_SSL
191        else:
192            self._verify_ssl = verify_ssl
193
194        self._is_mfa_login = False
195        self.__headers = None
196        self.__user = None
197        self.__auth_header = None
198        self.__token_expiry = None
199        self.__username = username
200        self.__password = password

Lazzy initialization of the MicroService of object.

This function might be usefull to use the object as a singleton at the backends. Using this function it is possible to instanciate an empty object and them set the attributes latter at the systems.

Arguments:
  • name: Name of the microservice, helps when exceptions are raised.
  • server_url: URL of the server that will be connected.
  • username: Username that will be logged on.
  • password: Variable to be converted to JSON and posted along with the request.
  • verify_ssl: Set if microservice will verify SSL certificate.
  • debug: If microservice will be used as debug mode. This will obrigate auth token refresh for each call.
  • default_timeout: Default timeout for Pumpwood calls.
  • **kwargs: Other parameters used for compatibility between versions.
Returns:

No return

Raises:
  • No particular Raises
@staticmethod
def angular_json(request_result) -> Any:
202    @staticmethod
203    def angular_json(request_result) -> Any:
204        r"""Convert text to Json removing any XSSI at the beging of JSON.
205
206        Some backends add `)]}',\n` at the beginning of the JSON data to
207        prevent injection of functions. This function remove this characters
208        if present.
209
210        Args:
211            request_result:
212                JSON request to be converted.
213
214        Returns:
215            No return
216
217        Raises:
218            PumpWoodJSONLoadError:
219                If it is not possible to load JSON from request data.
220        """
221        if request_result.text == '':
222            return None
223
224        string_start = ")]}',\n"
225        try:
226            if request_result.text[:6] == string_start:
227                return (orjson.loads(request_result.text[6:]))
228            else:
229                return (orjson.loads(request_result.text))
230        except Exception:
231            msg = "Can not decode to Json"
232            raise PumpWoodJSONLoadError(
233                message=msg, payload={"request_data": request_result.text})

Convert text to Json removing any XSSI at the beging of JSON.

Some backends add )]}',\n at the beginning of the JSON data to prevent injection of functions. This function remove this characters if present.

Arguments:
  • request_result: JSON request to be converted.
Returns:

No return

Raises:
  • PumpWoodJSONLoadError: If it is not possible to load JSON from request data.
def time_to_expiry(self) -> pandas.Timedelta:
235    def time_to_expiry(self) -> pd.Timedelta:
236        """Return time to token expiry.
237
238        Args:
239            No Args.
240
241        Returns:
242            Return time until token expiration.
243        """
244        if self.__token_expiry is None:
245            return None
246
247        now_datetime = pd.to_datetime(
248            datetime.datetime.now(datetime.UTC), utc=True)
249        time_to_expiry = self.__token_expiry - now_datetime
250        return time_to_expiry

Return time to token expiry.

Arguments:
  • No Args.
Returns:

Return time until token expiration.

def is_credential_set(self) -> bool:
252    def is_credential_set(self) -> bool:
253        """Check if username and password are set on object.
254
255        Args:
256            No Args.
257
258        Returns:
259            True if usename and password were set during object creation or
260            later with init function.
261        """
262        is_username_not_none = self.__username is not None
263        is_password_not_none = self.__password is not None
264        return is_username_not_none and is_password_not_none

Check if username and password are set on object.

Arguments:
  • No Args.
Returns:

True if usename and password were set during object creation or later with init function.

@classmethod
def is_invalid_token_response(cls, response: requests.models.Response) -> bool:
266    @classmethod
267    def is_invalid_token_response(cls, response: Response) -> bool:
268        """Check if reponse has invalid token error.
269
270        Args:
271            response:
272                Request reponse to check for invalid token.
273
274        Returns:
275            Return True if response has an invalid token status.
276        """
277        if response.status_code == 401:
278            return True
279        return False

Check if reponse has invalid token error.

Arguments:
  • response: Request reponse to check for invalid token.
Returns:

Return True if response has an invalid token status.

def confirm_mfa_code(self, mfa_login_data: dict) -> dict:
329    def confirm_mfa_code(self, mfa_login_data: dict) -> dict:
330        """Ask user to confirm MFA code to login.
331
332        Open an input interface at terminal for user to validate MFA token.
333
334        Args:
335            mfa_login_data:
336                Result from login request with 'mfa_token'
337                as key.
338
339        Returns:
340            Return login returned with MFA confimation.
341
342        Raise:
343            Raise error if reponse is not valid using error_handler.
344        """
345        code = input("## Please enter MFA code: ")
346        url = urljoin(
347            self.server_url, 'rest/registration/mfa-validate-code/')
348        mfa_response = requests.post(url, headers={
349            "X-PUMPWOOD-MFA-Autorization": mfa_login_data['mfa_token']},
350            json={"mfa_code": code}, timeout=self._default_timeout)
351        self.error_handler(mfa_response)
352
353        # Set _is_mfa_login true to indicate that login required MFA
354        self._is_mfa_login = True
355        return self.angular_json(mfa_response)

Ask user to confirm MFA code to login.

Open an input interface at terminal for user to validate MFA token.

Arguments:
  • mfa_login_data: Result from login request with 'mfa_token' as key.
Returns:

Return login returned with MFA confimation.

Raises:
  • Raise error if reponse is not valid using error_handler.
def login(self, force_refresh: bool = False) -> None:
357    def login(self, force_refresh: bool = False) -> None:
358        """Log microservice in using username and password provided.
359
360        Args:
361            force_refresh (bool):
362                Force token refresh despise still valid
363                according to self.__token_expiry.
364
365        Returns:
366            No return
367
368        Raises:
369            Exception:
370                If login response has status diferent from 200.
371        """
372        if not self.is_credential_set():
373            raise PumpWoodUnauthorized(
374                message="Microservice username or/and password not set")
375
376        # Check if expiry time is 1h from now
377        refresh_expiry = False
378        if self.__token_expiry is None:
379            refresh_expiry = True
380        else:
381            time_to_expiry = self.time_to_expiry()
382            if time_to_expiry < datetime.timedelta(hours=1):
383                refresh_expiry = True
384
385        # When if debug always refresh token
386        if refresh_expiry or force_refresh or self._debug:
387            login_data = self._login_resquest()
388            if 'mfa_token' in login_data.keys():
389                login_data = self.confirm_mfa_code(
390                    mfa_login_data=login_data)
391
392            self.set_auth_header(
393                auth_token='Token ' + login_data['token'],
394                token_expiry=pd.to_datetime(login_data['expiry']),
395                user=login_data["user"])
396        else:
397            # Token is not expired or envicted, them keep same token
398            return None

Log microservice in using username and password provided.

Arguments:
  • force_refresh (bool): Force token refresh despise still valid according to self.__token_expiry.
Returns:

No return

Raises:
  • Exception: If login response has status diferent from 200.
def logout(self, auth_header: dict = None) -> bool:
410    def logout(self, auth_header: dict = None) -> bool:
411        """Logout token.
412
413        Args:
414            auth_header:
415                Authentication header.
416
417        Returns:
418            True if logout was ok.
419        """
420        resp = self.request_post(
421            url='rest/registration/logout/',
422            data={}, auth_header=auth_header)
423        self._evict_auth_state()
424        return resp is None

Logout token.

Arguments:
  • auth_header: Authentication header.
Returns:

True if logout was ok.

def logout_all(self, auth_header: dict = None) -> bool:
426    def logout_all(self, auth_header: dict = None) -> bool:
427        """Logout all tokens from user.
428
429        Args:
430            auth_header (dict):
431                Authentication header.
432
433        Returns:
434            True if logout all was ok.
435        """
436        resp = self.request_post(
437            url='rest/registration/logoutall/',
438            data={}, auth_header=auth_header)
439        self._evict_auth_state()
440        return resp is None

Logout all tokens from user.

Arguments:
  • auth_header (dict): Authentication header.
Returns:

True if logout all was ok.

def get_auth_header(self) -> dict:
442    def get_auth_header(self) -> dict:
443        """Retrieve auth_header and token_expiry from object.
444
445        Args:
446            No Args.
447
448        Returns:
449            Return authorization header and token_expiry datetime from object.
450        """
451        # Copy the dictonary to avoid updating the original one
452        return copy.deepcopy({
453            "auth_header": self.__auth_header,
454            "token_expiry": self.__token_expiry})

Retrieve auth_header and token_expiry from object.

Arguments:
  • No Args.
Returns:

Return authorization header and token_expiry datetime from object.

def set_auth_header( self, auth_token: str, token_expiry: pandas.Timestamp, user: dict = None) -> dict:
456    def set_auth_header(self, auth_token: str,
457                        token_expiry: pd.Timestamp,
458                        user: dict = None) -> dict:
459        """Retrieve auth_header and token_expiry from object.
460
461        Args:
462            auth_token (str):
463                Auth token that will be set for authentication.
464            token_expiry (pd.Timestamp):
465                Token expiry time.
466            user (dict):
467                User information to be set on authetication.
468
469        Returns:
470            Return authorization header and token_expiry datetime from object.
471        """
472        # Copy the dictonary to avoid updating the original one
473        self.__auth_header = {
474            'Authorization': auth_token}
475        self.__token_expiry = token_expiry
476        self.__user = user
477        return True

Retrieve auth_header and token_expiry from object.

Arguments:
  • auth_token (str): Auth token that will be set for authentication.
  • token_expiry (pd.Timestamp): Token expiry time.
  • user (dict): User information to be set on authetication.
Returns:

Return authorization header and token_expiry datetime from object.

def is_superuser(self) -> bool:
479    def is_superuser(self) -> bool:
480        """Check if is superuser.
481
482        Args:
483            No Args.
484
485        Returns:
486            Return True if is superuser.
487        """
488        user = self.__user
489        if user is None:
490            return False
491        return user.get("is_superuser", False)

Check if is superuser.

Arguments:
  • No Args.
Returns:

Return True if is superuser.

@classmethod
def error_handler(cls, response):
558    @classmethod
559    def error_handler(cls, response):
560        """Handle request error.
561
562        Check if is a Json and propagate the error with
563        same type if possible. If not Json raises the content.
564
565        Args:
566            response:
567                response to be handled, it is a PumpWoodException
568                return it will raise the same exception at microservice
569                object.
570
571        Returns:
572            No return.
573
574        Raises:
575            PumpWoodOtherException:
576                If content-type is not application/json.
577            PumpWoodOtherException:
578                If content-type is application/json, but type not
579                present or not recognisable at `exceptions.exceptions_dict`.
580            Other PumpWoodException sub-types:
581                If content-type is application/json if type is present and
582                recognisable.
583
584        Example:
585            No example
586        """
587        if not response.ok:
588            utcnow = datetime.datetime.now(datetime.UTC)
589            response_content_type = response.headers['content-type']
590
591            # Request information
592            url = response.url
593            method = response.request.method
594            if 'application/json' not in response_content_type.lower():
595                # Raise the exception as first in exception deep.
596                exception_dict = [{
597                    "exception_url": url,
598                    "exception_method": method,
599                    "exception_utcnow": utcnow.isoformat(),
600                    "exception_deep": 1}]
601                raise PumpWoodOtherException(
602                    message=response.text, payload={
603                        "!exception_stack!": exception_dict})
604
605            # Build error stack
606            response_dict = cls.angular_json(response)
607
608            # Removing previous error stack
609            payload = copy.deepcopy(
610                response_dict.get("payload", {}))
611            exception_stack = copy.deepcopy(
612                payload.pop("!exception_stack!", []))
613
614            exception_deep = len(exception_stack)
615            exception_dict = {
616                "exception_url": url,
617                "exception_method": method,
618                "exception_utcnow": utcnow.isoformat(),
619                "exception_deep": exception_deep + 1
620            }
621            exception_stack.insert(0, exception_dict)
622            payload["!exception_stack!"] = exception_stack
623
624            ###################
625            # Propagate error #
626            # get exception using 'type' key at response data and get the
627            # exception from exceptions_dict at exceptions
628            exception_message = response_dict.get(
629                "message_not_fmt", response_dict.get("message", ""))
630            exception_type = response_dict.get("type")
631            TempPumpwoodException = exceptions_dict\
632                .get(exception_type, None)
633            if TempPumpwoodException is not None:
634                raise TempPumpwoodException(
635                    message=exception_message,
636                    status_code=response_dict.get(
637                        "status_code", response.status_code),
638                    payload=payload,
639                    was_translated=response_dict.get("was_translated", False),
640                    parallel=response_dict.get("parallel", False))
641            else:
642                # If token is invalid is at response, return a
643                # PumpWoodUnauthorized error
644                is_invalid_token = cls.is_invalid_token_response(response)
645                response_dict["!exception_stack!"] = exception_stack
646                if is_invalid_token:
647                    raise PumpWoodUnauthorized(
648                        message="Invalid token", payload=payload)
649                else:
650                    # If the error is not mapped return a
651                    # PumpWoodOtherException limiting the message size to 1k
652                    # characters
653                    raise PumpWoodOtherException(
654                        message="Not mapped exception JSON",
655                        payload=response_dict)

Handle request error.

Check if is a Json and propagate the error with same type if possible. If not Json raises the content.

Arguments:
  • response: response to be handled, it is a PumpWoodException return it will raise the same exception at microservice object.
Returns:

No return.

Raises:
  • PumpWoodOtherException: If content-type is not application/json.
  • PumpWoodOtherException: If content-type is application/json, but type not present or not recognisable at exceptions.exceptions_dict.
  • Other PumpWoodException sub-types: If content-type is application/json if type is present and recognisable.
Example:

No example

def request_post( self, url: str, data: <built-in function any>, files: list = None, auth_header: dict = None, parameters: None | dict = {}) -> <built-in function any>:
808    def request_post(self, url: str, data: any, files: list = None,
809                     auth_header: dict = None,
810                     parameters: None | dict = {}) -> any:
811        """Make a POST a request to url with data as multipart/json payload.
812
813        Args:
814            url:
815                URL to make the request, already with server url.
816            data:
817                Data to be used as Json payload.
818            files:
819                A dictonary with file data, files will be set on field
820                corresponding.to dictonary key.
821                `{'file1': open('file1', 'rb'), {'file2': open('file2', 'rb')}`
822            parameters:
823                URL parameters.
824            auth_header:
825                AuthHeader to substitute the microservice original
826                at the request (user impersonation).
827
828        Returns:
829            Return the post response data.
830
831        Raises:
832            PumpWoodException sub-types:
833                Response is passed to error_handler.
834        """
835        parameters = {} if parameters is None else parameters
836
837        post_url = urljoin(self.server_url, url)
838        dumped_parameters = self._dump_query_parameters(parameters=parameters)
839        response = None
840        if files is None:
841            response = self._request_post_json(
842                post_url=post_url, data=data, auth_header=auth_header,
843                parameters=dumped_parameters)
844        else:
845            response = self._request_post_multi(
846                post_url=post_url, data=data, files=files,
847                auth_header=auth_header, parameters=dumped_parameters)
848
849        # Handle errors and re-raise if Pumpwood Exceptions
850        self.error_handler(response)
851        return self._treat_response_for_file(response=response)

Make a POST a request to url with data as multipart/json payload.

Arguments:
  • url: URL to make the request, already with server url.
  • data: Data to be used as Json payload.
  • files: A dictonary with file data, files will be set on field corresponding.to dictonary key. {'file1': open('file1', 'rb'), {'file2': open('file2', 'rb')}
  • parameters: URL parameters.
  • auth_header: AuthHeader to substitute the microservice original at the request (user impersonation).
Returns:

Return the post response data.

Raises:
  • PumpWoodException sub-types: Response is passed to error_handler.
def request_get( self, url: str, parameters: None | dict = None, auth_header: dict = None, use_disk_cache: bool = False, disk_cache_expire: int = None, disk_cache_tag_dict: dict = None) -> Any:
853    def request_get(self, url: str, parameters: None | dict = None,
854                    auth_header: dict = None,
855                    use_disk_cache: bool = False,
856                    disk_cache_expire: int = None,
857                    disk_cache_tag_dict: dict = None) -> Any:
858        """Make a GET a request to url with data as JSON payload.
859
860        Add the auth_header acording to login information and refresh token
861        if auth_header=None and object token is expired.
862
863        Args:
864            url (str):
865                URL to make the request.
866            parameters (dict):
867                URL parameters to make the request.
868            auth_header (dict):
869                Auth header to substitute the microservice original
870                at the request (user impersonation).
871            use_disk_cache (bool):
872                If set true, get request will use local cache to reduce
873                the requests to the backend.
874            disk_cache_expire (int):
875                Time in seconds to expire the cache, it None it will
876                use de default set be PumpwoodCache.
877            disk_cache_tag_dict (dict):
878                Dictionary to be used as a tag on get request.
879
880        Returns:
881            Return the post reponse data.
882
883        Raises:
884            PumpWoodException sub-types:
885                Raise exception if reponse is not 2XX and if 'type' key on
886                JSON payload if found at exceptions_dict. Use the same
887                exception, message and payload.
888            PumpWoodOtherException:
889                If exception type is not found or return is not a json.
890        """
891        parameters = {} if parameters is None else parameters
892
893        request_header = self._check_auth_header(auth_header)
894        # If is set to use diskcache, it will create a hash cash using
895        # the query paramerers, url and user access token. The
896        # hash will be used as index, not exposing the token at cache
897        # database
898        hash_dict = None
899        if use_disk_cache:
900            hash_dict = RequestGetCacheHash(
901                context='pumpwood_communication-request_get',
902                authorization=request_header['Authorization'],
903                parameters=parameters,
904                url=url)
905
906            cache_results = default_cache.get(hash_dict=hash_dict)
907            if cache_results is not None:
908                msg = "get from cache url[{url}]".format(url=url)
909                logger.info(msg)
910                return cache_results
911
912        dumped_parameters = self._dump_query_parameters(parameters=parameters)
913        get_url = urljoin(self.server_url, url)
914        response = requests.get(
915            get_url, verify=self._verify_ssl, headers=request_header,
916            params=dumped_parameters, timeout=self._default_timeout)
917
918        # If token is expired, refresh it
919        retry_with_login = (
920            self.is_invalid_token_response(response) and
921            auth_header is None)
922        if retry_with_login:
923            time.sleep(0.5)
924            self.login(force_refresh=True)
925            request_header = self._check_auth_header(auth_header=auth_header)
926            response = requests.get(
927                get_url, verify=self._verify_ssl, headers=request_header,
928                params=dumped_parameters, timeout=self._default_timeout)
929
930        # Re-raise Pumpwood exceptions
931        self.error_handler(response=response)
932        results = self._treat_response_for_file(response=response)
933
934        # If is set to use cache for this calls, set the local cache
935        if use_disk_cache and not results.get('__file__', False):
936            default_cache.set(
937                hash_dict=hash_dict, value=results,
938                expire=disk_cache_expire,
939                tag_dict=disk_cache_tag_dict)
940        return results

Make a GET a request to url with data as JSON payload.

Add the auth_header acording to login information and refresh token if auth_header=None and object token is expired.

Arguments:
  • url (str): URL to make the request.
  • parameters (dict): URL parameters to make the request.
  • auth_header (dict): Auth header to substitute the microservice original at the request (user impersonation).
  • use_disk_cache (bool): If set true, get request will use local cache to reduce the requests to the backend.
  • disk_cache_expire (int): Time in seconds to expire the cache, it None it will use de default set be PumpwoodCache.
  • disk_cache_tag_dict (dict): Dictionary to be used as a tag on get request.
Returns:

Return the post reponse data.

Raises:
  • PumpWoodException sub-types: Raise exception if reponse is not 2XX and if 'type' key on JSON payload if found at exceptions_dict. Use the same exception, message and payload.
  • PumpWoodOtherException: If exception type is not found or return is not a json.
def request_delete(self, url, parameters: dict = None, auth_header: dict = None):
942    def request_delete(self, url, parameters: dict = None,
943                       auth_header: dict = None):
944        """Make a DELETE a request to url with data as Json payload.
945
946        Args:
947            url:
948                Url to make the request.
949            parameters:
950                Dictionary with Urls parameters.
951            auth_header:
952                Auth header to substitute the microservice original
953                at the request (user impersonation).
954
955        Returns:
956            Return the delete reponse payload.
957
958        Raises:
959            PumpWoodException sub-types:
960                Raise exception if reponse is not 2XX and if 'type' key on
961                JSON payload if found at exceptions_dict. Use the same
962                exception, message and payload.
963            PumpWoodOtherException:
964                If exception type is not found or return is not a json.
965        """
966        request_header = self._check_auth_header(auth_header)
967        dumped_parameters = self._dump_query_parameters(parameters=parameters)
968
969        post_url = self.server_url + url
970        response = requests.delete(
971            post_url, verify=self._verify_ssl, headers=request_header,
972            params=dumped_parameters, timeout=self._default_timeout)
973
974        # Retry request if token is not valid forcing token renew
975        retry_with_login = (
976            self.is_invalid_token_response(response) and
977            auth_header is None)
978        if retry_with_login:
979            time.sleep(0.5)
980            self.login(force_refresh=True)
981            request_header = self._check_auth_header(auth_header=auth_header)
982            response = requests.delete(
983                post_url, verify=self._verify_ssl, headers=request_header,
984                params=dumped_parameters, timeout=self._default_timeout)
985
986        # Re-raise Pumpwood Exceptions
987        self.error_handler(response)
988        return self.angular_json(response)

Make a DELETE a request to url with data as Json payload.

Arguments:
  • url: Url to make the request.
  • parameters: Dictionary with Urls parameters.
  • auth_header: Auth header to substitute the microservice original at the request (user impersonation).
Returns:

Return the delete reponse payload.

Raises:
  • PumpWoodException sub-types: Raise exception if reponse is not 2XX and if 'type' key on JSON payload if found at exceptions_dict. Use the same exception, message and payload.
  • PumpWoodOtherException: If exception type is not found or return is not a json.
def get_user_info( self, auth_header: dict = None, use_disk_cache: bool = False, disk_cache_expire: int = None) -> dict:
 990    def get_user_info(self, auth_header: dict = None,
 991                      use_disk_cache: bool = False,
 992                      disk_cache_expire: int = None) -> dict:
 993        """Get user info.
 994
 995        Args:
 996            auth_header (dict): = None
 997                AuthHeader to substitute the microservice original at
 998                request. If not passed, microservice object auth_header
 999                will be used.
1000            use_disk_cache (bool):
1001                It possible use disk cache.
1002            disk_cache_expire (int):
1003                Set a time to expire the cache. If not passed env variable
1004                ``PUMPWOOD_COMMUNICATION__AUTHORIZATION_CACHE_TIMEOUT``
1005                env variable (legacy spelling supported), default 60 s.
1006
1007        Returns:
1008            A serialized user object with information of the logged user.
1009        """
1010        url = "rest/registration/retrieveauthenticateduser/"
1011        temp_disk_cache_expire = (
1012            disk_cache_expire
1013            if disk_cache_expire is not None else
1014            AUTHORIZATION_CACHE_TIMEOUT)
1015
1016        user_info = self.request_get(
1017            url=url, auth_header=auth_header, use_disk_cache=use_disk_cache,
1018            disk_cache_expire=temp_disk_cache_expire)
1019        return user_info

Get user info.

Arguments:
  • auth_header (dict): = None AuthHeader to substitute the microservice original at request. If not passed, microservice object auth_header will be used.
  • use_disk_cache (bool): It possible use disk cache.
  • disk_cache_expire (int): Set a time to expire the cache. If not passed env variable PUMPWOOD_COMMUNICATION__AUTHORIZATION_CACHE_TIMEOUT env variable (legacy spelling supported), default 60 s.
Returns:

A serialized user object with information of the logged user.