class TokenContext(object):
    """TokenContext provides payload for CallbackJwtProvider.get_token(TokenContext)"""

    def __init__(
        self,
        identity,
        operation,
        force_reload=False,
        service=None
    ):
        self._operation = operation
        self._identity = identity
        self._force_reload = force_reload
        self._service = service

    @property
    def operation(self):
        """Operation for which token is needed."""
        return self._operation

    @property
    def identity(self):
        """Identity that should be used in access token."""
        return self._identity

    @property
    def force_reload(self):
        """You can set up token cache in CachingCallbackProvider.get_token and reset cached token if True."""
        return self._force_reload

    @property
    def service(self):
        """Service for which token is needed."""
        return self._service class RawCardContent(object):
    """
    RawCardContent provides content of Virgil Card.
    """

    def __init__(
        self,
        identity,  # type: str
        public_key,  # type: Union[bytearray, bytes, Tuple[int], List[int]]
        created_at,  # type datetime
        version="5.0",  # type: str
        previous_card_id=None,  # type: str
    ):
        self._identity = identity
        self._public_key = public_key
        self._version = version
        self._created_at = created_at
        self._previous_card_id = previous_card_id
        self._content_snapshot = None

    def to_json(self):
        """
        Raw card content json representation.

        Returns:
            Serialize raw card content to json.
        """
        return OrderedDict({
            "identity": self.identity,
            "public_key": self.public_key,
            "version": self.version,
            "created_at": self.created_at,
            "previous_card_id": self.previous_card_id,
            "content_snapshot": self.content_snapshot
        })

    @classmethod
    def from_snapshot(cls, content_snapshot):
        # type: (dict) -> RawCardContent
        """
        RawCardContent deserializer from snapshot representation.

        Args:
            content_snapshot: RawCardContent serialized snapshot.

        Returns:
            Loaded RawCardContent instance.
        """
        card_content = cls.__new__(cls)
        loaded_snapshot = Utils.json_loads(Utils.b64_decode(content_snapshot))
        card_content._identity = loaded_snapshot["identity"]
        card_content._public_key = loaded_snapshot["public_key"]
        card_content._version = loaded_snapshot["version"]
        card_content._created_at = loaded_snapshot["created_at"]
        if "previous_card_id" in loaded_snapshot.keys():
            card_content._previous_card_id = loaded_snapshot["previous_card_id"]
        else:
            card_content._previous_card_id = None
        card_content._content_snapshot = content_snapshot
        return card_content

    @classmethod
    def from_signed_model(cls, card_crypto, raw_singed_model):
        # type: (Any, RawSignedModel) -> RawCardContent
        """
        RawCardContent deserializer from RawSignedModel representation.

        Args:
            card_crypto: CardCrypto witch provides crypto operations.
            raw_singed_model: Card raw signed model.

        Returns:
            Loaded RawCardContent instance.
        """
        card = cls.from_snapshot(raw_singed_model.content_snapshot)
        card._public_key = card_crypto.import_public_key(bytearray(Utils.b64_decode(card._public_key)))
        return card

    @property
    def identity(self):
        """
        Gets the identity value that can be anything which identifies the user in your application.

        Returns:
            User identity.
        """
        return self._identity

    @property
    def public_key(self):
        """
        Gets the public key.

        Returns:
            Public key.
        """
        return self._public_key

    @property
    def version(self):
        """
        Gets the version of the card.

        Returns:
            Card version.
        """
        return self._version

    @property
    def created_at(self):
        """
        Gets the date and time fo card creation in UTC.

        Returns:
            Creation date in UTC datetime.
        """
        return self._created_at

    @property
    def previous_card_id(self):
        """
        Get previous Card ID that current card is used to override to.

        Returns:
            Previous card id.
        """
        return self._previous_card_id

    @property
    def content_snapshot(self):
        """
        RawCardContent snapshot.

        Returns:
            Snapshot of RawCardContent.
        """
        if not self._content_snapshot:
            content = {
                "identity": self._identity,
                "public_key": Utils.b64encode(self._public_key),
                "version": self._version,
                "created_at": self._created_at,
            }
            if self._previous_card_id:
                content.update({"previous_card_id": self._previous_card_id})
            self._content_snapshot = Utils.b64encode(
                Utils.json_dumps(content, sort_keys=True, separators=(',', ':')).encode()
            )
        return self._content_snapshot class ExpiredAuthorizationClientException(ClientException):
    pass class RawSignedModel(object):
    """
    The RawSignedModel provides transitional model of <see cref="Card"/>
    and used by CardClient.
    """

    def __init__(
        self,
        content_snapshot,  # type: Union[bytes, bytearray]
        signatures=None  # List[RawSignature]
    ):
        self._content_snapshot = content_snapshot
        self._signatures = signatures or []

    def to_json(self):
        """
        RawSignedModel json representation.
        """
        return Utils.json_dumps(
            {
                "content_snapshot": self.content_snapshot,
                "signatures": list(map(lambda x: x.to_json(), self.signatures))
            },
            separators=(',', ':'),
            sort_keys=True
        )

    def to_string(self):
        """
        Serialize to base64 encoded string.

        Returns:
            Base64 encoded string.
        """
        return Utils.b64encode(self.to_json().encode())

    def add_signature(self, signature):
        # type: (RawSignature) -> None
        """
        Add signature to RawSignedModel list.

        Args:
            signature: Card signature.

        Raises:
            ValueError: Attempt to add existing signature.
        """
        if signature.signer in list(map(lambda x: x.signer, self._signatures)):
            raise ValueError("Attempt to add an existing signature")
        else:
            self._signatures.append(signature)

    @property
    def content_snapshot(self):
        """
        Snapshot of RawCardContent.
        """
        return self._content_snapshot

    @property
    def signatures(self):
        """
        A list of signatures.
        """
        return self._signatures

    @classmethod
    def generate(cls, public_key, identity, created_at, previous_card_id=None):
        # type: (Union[bytearray, Tuple[int], List[int]], str, int, Optional[str]) -> RawSignedModel
        """
        Generate card RawSignedModel.

        Args:
            public_key: Card public key bytes.
            identity: Unique card identity.
            created_at: Creation timestamp.
            previous_card_id: Previous card ID.

        Returns:
            Generate RawSignedModel instance.
        """
        raw_card = RawCardContent(
            identity=identity,
            public_key=public_key,
            created_at=created_at,
            previous_card_id=previous_card_id
        )
        return RawSignedModel(raw_card.content_snapshot)

    @classmethod
    def from_string(cls, raw_signed_model_string):
        # type: (str) -> RawSignedModel
        """Deserialize RawSignedModel from base64 encoded string."""
        return cls.from_json(Utils.b64_decode(raw_signed_model_string).decode())

    @classmethod
    def from_json(cls, raw_signed_model_json):
        # type: (Union[str, bytes, bytearray]) -> RawSignedModel
        """Deserialize RawSignedModel from json representation."""
        loaded_json = Utils.json_loads(raw_signed_model_json)
        content_snapshot = loaded_json["content_snapshot"]
        signatures = []
        for sign in loaded_json["signatures"]:
            if "signature_snapshot" in sign.keys():
                signature = RawSignature(
                    sign["signer"],
                    Utils.b64decode(sign["signature"]),
                    Utils.b64decode(sign["signature_snapshot"])
                )
                signatures.append(signature)
            else:
                signature = RawSignature(
                    sign["signer"],
                    Utils.b64decode(sign["signature"])
                )
                signatures.append(signature)
        return RawSignedModel(content_snapshot, signatures) class Utils(object):

    @staticmethod
    def b64_decode(source):
        """Decode base64, padding being optional.

        Args:
            source: Base64 data as an ASCII byte string

        Returns:
            The decoded byte string.

        """
        try:
            if isinstance(source, bytes):
                return base64.urlsafe_b64decode(source)
            return base64.urlsafe_b64decode(bytearray(source, "utf-8"))
        except (binascii.Error, TypeError) as e:
            missing_padding = len(source) % 4
            if missing_padding != 0:
                if isinstance(source, str) or check_unicode(source):
                    source += '=' * (4 - missing_padding)
                if isinstance(source, bytes) or isinstance(source, bytearray):
                    source += b'=' * (4 - missing_padding)
            if isinstance(source, bytes):
                return base64.urlsafe_b64decode(source)
            return base64.urlsafe_b64decode(bytearray(source, "utf-8"))

    @staticmethod
    def b64_encode(source):
        """
        Removes any `=` used as padding from the encoded string.

        Args:
            Data for encoding.

        Returns:
            Encoded data without '=' sign
        """
        if isinstance(source, bytes):
            encoded = base64.urlsafe_b64encode(source)
        else:
            encoded = base64.urlsafe_b64encode(bytearray(source, "utf-8"))
        return bytearray(encoded).decode().rstrip("=")

    @staticmethod
    def strtobytes(source):
        # type: (str) -> Tuple[*int]
        """Convert string to bytes tuple used for all crypto methods."""
        return tuple(bytearray(source))

    @classmethod
    def b64tobytes(cls, source):
        # type: (str) -> Tuple[*int]
        """Convert source to bytearray and encode using base64."""
        return cls.strtobytes(cls.b64decode(source))

    @staticmethod
    def b64encode(source):
        # type: (Union[str, bytes]) -> str
        """Convert source to bytearray and encode using base64."""
        return base64.b64encode(bytearray(source)).decode("utf-8", "ignore")

    @staticmethod
    def b64decode(source):
        # type: (Union[str, bytes]) -> str
        """Convert source to bytearray and decode using base64."""
        if isinstance(source, bytes):
            return base64.b64decode(source)
        return base64.b64decode(bytearray(source, "utf-8"))

    @staticmethod
    def json_loads(source):
        # type: (Union[str, bytes, bytearray]) -> dict
        """Convert source to bytearray and deserialize from json to python dict object."""
        if isinstance(source, bytes):
            return json.loads(bytearray(source).decode())
        return json.loads(bytearray(source, "utf-8").decode())

    @staticmethod
    def json_dumps(source, *args, **kwargs):
        # type: (object) -> str
        """Convert python dict to json string"""
        return json.dumps(source, *args, **kwargs)

    @staticmethod
    def to_timestamp(date):
        # type: (datetime) -> Union[int, str]
        epoch = datetime.datetime(1970, 1, 1)
        return int((date - epoch).total_seconds())

    @staticmethod
    def raise_from(exception):
        """Supress long traceback for custom exceptions Python 3, show only important exception."""
        exception.__cause__ = None
        raise exception

    @staticmethod
    def normalize_string(source):
        return normalize_string(source)

    @staticmethod
    def check_unicode(source):
        return check_unicode(source) class CardVerificationException(Exception):
    """
    Represents errors occurred during card validation.
    """
    pass class Card(object):
    """
    The Card class is the main entity of Virgil Services. Every user/device is
    represented with a Virgil Card which contains a public key and information about identity.
    """

    def __init__(
        self,
        card_id,  # type: str
        identity,  # type: str
        public_key,  # type: PublicKey
        version,  # type: str
        created_at,  # type: datetime
        signatures,  # type: list[CardSignature]
        previous_card_id,  # type: str
        content_snapshot,  # type: bytearray
        is_outdated=False  # type: bool
    ):
        # type: (...) -> None
        super(Card, self).__init__(identity, public_key, version, created_at, previous_card_id)
        self._id = card_id
        self._identity = identity
        self._version = version
        self._created_at = created_at
        self._previous_card_id = previous_card_id
        self._public_key = public_key
        self.__signatures = signatures
        self.previous_card = None
        self._content_snapshot = content_snapshot
        self.is_outdated = is_outdated

    @classmethod
    def __generate_card_id(cls, card_crypto, content_snapshot):
        # type: (Any, dict) -> str
        """
        Generate card id from content snapshot
        Args:
            card_crypto: Users CardCrypto witch provides cryptographic operations.
            content_snapshot: Card content snapshot
        Returns:
            Generated Card id.
        """
        fingerprint = card_crypto.generate_sha512(bytearray(content_snapshot))
        card_id = binascii.hexlify(bytearray(fingerprint)[:32]).decode()
        return card_id

    @classmethod
    def from_snapshot(cls, content_snapshot):
        # type: (str) -> Card
        """
        Creates card from content snapshot.
        Args:
            content_snapshot: Model content snapshot.

        Returns:
            Card created from model content snapshot.
        """
        card_content = cls.__new__(cls)
        loaded_snapshot = Utils.json_loads(Utils.b64_decode(content_snapshot))
        card_content._identity = loaded_snapshot["identity"]
        card_content._public_key = loaded_snapshot["public_key"]
        card_content._version = loaded_snapshot["version"]
        card_content._created_at = loaded_snapshot["created_at"]
        if "previous_card_id" in loaded_snapshot.keys():
            card_content._previous_card_id = loaded_snapshot["previous_card_id"]
        else:
            card_content._previous_card_id = None
        card_content._content_snapshot = content_snapshot
        return card_content

    @classmethod
    def from_signed_model(cls, card_crypto, raw_singed_model, is_outdated=False):
        # type: (Any, RawSignedModel, bool) -> Card
        """
        Creates card from SignedModel snapshot and signatures.
        Args:
            card_crypto: Users CardCrypto witch provides cryptographic operations.
            raw_singed_model: Card RawSignedModel
            is_outdated: State of obsolescence

        Returns:
            Card created from RawSignedModel.
        """
        card = cls.from_snapshot(raw_singed_model.content_snapshot)
        card.previous_card = None
        card.is_outdated = is_outdated

        card._id = cls.__generate_card_id(card_crypto, Utils.b64_decode(raw_singed_model.content_snapshot))
        card._public_key = card_crypto.import_public_key(bytearray(Utils.b64_decode(card._public_key)))
        signatures = list()
        if raw_singed_model.signatures:
            for sign in raw_singed_model.signatures:
                if isinstance(sign, dict):
                    if "snapshot" in sign.keys():
                        card_signature = CardSignature(
                            sign["signer"],
                            bytearray(Utils.b64decode(sign["signature"])),
                            sign["snapshot"]
                        )
                    else:
                        card_signature = CardSignature(sign["signer"], bytearray(Utils.b64decode(sign["signature"])))
                    signatures.append(card_signature)
                if isinstance(sign, CardSignature):
                    card_signature = sign
                    signatures.append(card_signature)
                if isinstance(sign, RawSignature):
                    card_signature = CardSignature(sign.signer, sign.signature, sign.snapshot)
                    signatures.append(card_signature)
        card.__signatures = signatures
        return card

    @property
    def id(self):
        """
        Gets the Card ID that uniquely identifies the Card in Virgil Services.
        Returns:
            Card id.
        """
        return self._id

    @property
    def identity(self):
        """
        Gets the identity value that can be anything which identifies the user in your application.
        Returns:
            User identity.
        """
        return self._identity

    @property
    def public_key(self):
        """
        Gets the public key.
        Returns:
            Public key.
        """
        return self._public_key

    @property
    def version(self):
        """
        Gets the version of the card.
        Returns:
            Card version.
        """
        return self._version

    @property
    def created_at(self):
        """
        Gets the date and time fo card creation in UTC.
        Returns:
            Creation date in UTC datetime.
        """
        return self._created_at

    @property
    def previous_card_id(self):
        """
        Get previous Card ID that current card is used to override to.
        Returns:
            Previous card id.
        """
        return self._previous_card_id

    @property
    def signatures(self):
        """
        Gets a list of signatures.
        Returns:
            List of signatures
        """
        return self.__signatures

    @property
    def content_snapshot(self):
        """
        Card content snapshot
        Returns:
            Card snapshot.
        """
        if not self._content_snapshot:
            content = {
                "identity": self._identity,
                "public_key": Utils.b64encode(self._public_key.raw_key),
                "version": self._version,
                "created_at": self._created_at,
            }
            if self._previous_card_id:
                content.update({"previous_card_id": self._previous_card_id})
            self._content_snapshot = Utils.b64encode(
                Utils.json_dumps(content, sort_keys=True, separators=(',', ':')).encode()
            )
        return self._content_snapshot class VirgilCardVerifier(CardVerifier):
    """
    The VirgilCardVerifier represents card verification process.
    """

    def __init__(
        self,
        crypto,
        verify_self_signature=True,  # type: bool
        verify_virgil_signature=True,  # type: bool
        white_lists=list()  # type: List[WhiteList]
    ):
        self._crypto = crypto
        self.verify_self_signature = verify_self_signature
        self.verify_virgil_signature = verify_virgil_signature
        self.__white_lists = white_lists
        self.__virgil_public_key_base64 = "MCowBQYDK2VwAyEAljOYGANYiVq1WbvVvoYIKtvZi2ji9bAhxyu6iV/LF8M="

    def verify_card(self, card):
        # type: (Card) -> bool
        """
        To verify the specified card.

        To set up rule for verification of self signature use verify_self_signature.
        To set up rule for verification of virgil service signature use verify_virgil_signature.
        To set up Whitelists use white_lists.

        Args:
            card: The instance of Card to verify.

        Returns:
            True is card is verified according to set rules, otherwise False.
        """
        if self.verify_self_signature and not self.__validate_signer_signature(
                card,
                card.public_key,
                ModelSigner.SELF_SIGNER
        ):
            return False

        if self.verify_virgil_signature and not self.__validate_signer_signature(
                card,
                self.__get_public_key(self.__virgil_public_key_base64),
                ModelSigner.VIRGIL_SIGNER
        ):
            return False

        if not any(self.white_lists):
            return True

        signers = list(map(lambda x: x.signer, card.signatures))
        verifiers_credentials_lists = list(map(lambda x: x.verifiers_credentials, self.white_lists))

        for verifiers_credentials in verifiers_credentials_lists:

            intersected_creds = list(filter(lambda x: x.signer in signers, verifiers_credentials))

            if not any(intersected_creds):
                return False

            for intersected_cred in intersected_creds:
                signer_public_key = self.__get_public_key(intersected_cred.public_key_base64)
                if self.__validate_signer_signature(card, signer_public_key, intersected_cred.signer):
                    break
                if intersected_cred is intersected_creds[-1]:
                    return False
        return True

    def __get_public_key(self, signer_public_key_base64):
        public_key_bytes = Utils.b64_decode(signer_public_key_base64)
        return self._crypto.import_public_key(bytearray(public_key_bytes))

    def __validate_signer_signature(self, card, signer_public_key, signer_type):
        signature = None
        for sign in card.signatures:
            if sign.signer == signer_type:
                signature = sign
                break

        if signature:
            if signature.snapshot:
                extended_snapshot = bytearray(Utils.b64_decode(card.content_snapshot))\
                                    + bytearray(Utils.b64_decode(signature.snapshot))
            else:
                extended_snapshot = bytearray(Utils.b64_decode(card.content_snapshot))

            if self._crypto.verify_signature(
                bytearray(signature.signature),
                extended_snapshot,
                signer_public_key
            ):
                return True
        return False

    @property
    def white_lists(self):
        """Get white lists."""
        return self.__white_lists

    @white_lists.setter
    def white_lists(self, value):
        if value:
            self.__white_lists = list()
            if not isinstance(value, list):
                self.__white_lists += [value]
            else:
                self.__white_lists += value class CardClient(BaseCardClient):
    """
    The CardClient class provides operations with Virgil Cards service.
    """

    def __init__(
        self,
        api_url="https://api.virgilsecurity.com",  # type: str
        connection=None  # type: ServiceConnection
    ):
        self._api_url = api_url
        self.__connection = connection or ServiceConnection(
            self.api_url,
            adapters=[VirgilAgentAdapter("sdk", __version__)]
        )

    def publish_card(self, raw_card, token):
        # type: (RawSignedModel, str) -> RawSignedModel
        """
        Publishes card in Virgil Cards service.

        Args:
            raw_card: An instance of RawSignedModel class.
            token: The string representation of Jwt token.

        Returns:
            Published raw card.
        """
        if not raw_card:
            raise ValueError("Missing raw card")

        if not token:
            raise ValueError("Missing JWT token")

        request = Request(
            "/card/v5",
            Utils.json_loads(raw_card.to_json()),
            method=Request.POST
        )

        request.authorization(token)
        response, headers = self.__connection.send(request)
        return RawSignedModel(**response)

    def search_card(self, identity, token):
        # type: (Union[str, list], str) -> List[RawSignedModel]
        """
        Searches a cards on Virgil Services by specified identity.

        Args:
            identity: The identity (or list of identity).
            token: The string representation of Jwt token.

        Returns:
           A list of found cards in raw form.
        """
        if not identity:
            raise ValueError("Missing identity")

        if not token:
            raise ValueError("Missing JWT token")

        if isinstance(identity, str) or Utils.check_unicode(identity):
            request_body = {"identity": identity}
        elif isinstance(identity, list):
            request_body = {"identities": identity}
        else:
            raise ValueError("Wrong identity(ies) type")

        request = Request(
            "/card/v5/actions/search",
            request_body,
            Request.POST,
        )

        request.authorization(token)

        response, headers = self.__connection.send(request)
        cards = self.__parse_cards_from_response(response)

        return cards

    def get_card(self, card_id, token):
        # type: (str, str) -> Tuple[RawSignedModel, bool]
        """
        Gets a card from Virgil Services by specified card ID.

        Args:
            card_id: The Card ID.
            token: The string representation of Jwt token.

        Returns:
            An instance of RawSignedModel class and flag,
            which determines whether or not this raw card is superseded.

        Raises:
            ValueError: Missed argument.
        """
        if not card_id:
            raise ValueError("Missing card id")

        if not token:
            raise ValueError("Missing access token")

        request = Request(
            "/card/v5/{}".format(card_id),
        )

        request.authorization(token)

        response, headers = self.__connection.send(request)

        card_raw = RawSignedModel(**response)

        superseded = False
        if headers and "X-VIRGIL-IS-SUPERSEEDED" in headers.keys():
            if headers["X-VIRGIL-IS-SUPERSEEDED"]:
                superseded = True

        return card_raw, superseded

    @property
    def api_url(self):
        """
        Get service url.
        """
        return self._api_url

    def __parse_cards_from_response(self, response):
        if response:
            result = list()
            for card in response:
                result.append(RawSignedModel(**card))
            return result
        else:
            return response class ModelSigner(object):
    """
    The ModelSigner class provides signing operation for RawSignedModel.
    """

    SELF_SIGNER = "self"
    VIRGIL_SIGNER = "virgil"

    def __init__(
        self,
        card_crypto
    ):
        self.__card_crypto = card_crypto

    def sign(self, model, signer, signer_private_key, signature_snapshot=None, extra_fields=None):
        # type: (RawSignedModel, str, VirgilPrivateKey, Union[bytearray, bytes], dict) -> None
        """
        Adds signature to the specified RawSignedModel using specified signer.

        Args:
            model: The instance of RawSignedModel to be signed.
            signer:
            signer_private_key: The instance of PrivateKey to sign with.
            signature_snapshot: Some additional raw bytes to be signed with model.
            extra_fields: Dictionary with additional data to be signed with model.
        """
        if model.signatures:
            if any(list(filter(lambda x: x.signer == signer, model.signatures))):
                raise ValueError("The model already has this signature")

        if extra_fields and not signature_snapshot:
            signature_snapshot = bytearray(Utils.json_dumps(extra_fields).encode())

        if signature_snapshot:
            extended_snapshot = Utils.b64encode(bytearray(Utils.b64_decode(model.content_snapshot)) + bytearray(signature_snapshot))
        else:
            extended_snapshot = model.content_snapshot

        signature_bytes = self.__card_crypto.generate_signature(
            bytearray(Utils.b64_decode(extended_snapshot)),
            signer_private_key
        )

        signature = RawSignature(signer, bytearray(signature_bytes), signature_snapshot)
        model.add_signature(signature)

    def self_sign(self, model, signer_private_key, signature_snapshot=None, extra_fields=None):
        # type: (RawSignedModel, VirgilPrivateKey, Union[bytearray, bytes], dict) -> None
        """
        Adds owner's signature to the specified RawSignedModel using specified signer.

        Args:
            model: The instance of RawSignedModel to be signed.
            signer_private_key: The instance of PrivateKey to sign with.
            signature_snapshot: Some additional raw bytes to be signed with model.
            extra_fields: Dictionary with additional data to be signed with model.
        """
        if extra_fields and not signature_snapshot:
            signature_snapshot = Utils.json_dumps(extra_fields).encode()
        self.sign(model, self.SELF_SIGNER, signer_private_key, signature_snapshot)
import datetime
import sys
from virgil_sdk.jwt.token_context import TokenContext
from virgil_sdk.cards.raw_card_content import RawCardContent
from virgil_sdk.client import RawSignedModel, ExpiredAuthorizationClientException
from virgil_sdk.utils import Utils
from virgil_sdk.verification import CardVerificationException
from .card import Card
from virgil_sdk.verification.virgil_card_verifier import VirgilCardVerifier
from virgil_sdk.client.card_client import CardClient
from virgil_sdk.signers.model_signer import ModelSigner
    from json import JSONDecodeError
if sys.version_info[0] == 3 and sys.version_info[1] != 4:
else:
    class JSONDecodeError(Exception):
        pass


class CardManager(object):
    """The CardsManager class provides a list of methods to manage the VirgilCard entities."""

    def __init__(
        self,
        card_crypto,
        access_token_provider,
        card_verifier,
        sign_callback=None,
        api_url="https://api.virgilsecurity.com",
        retry_on_unauthorized=False
    ):
        self._card_crypto = card_crypto
        self._model_signer = None
        self._card_client = None
        self._card_verifier = card_verifier
        self._sign_callback = sign_callback
        self._access_token_provider = access_token_provider
        self.__retry_on_unauthorized = retry_on_unauthorized
        self.__api_url = api_url

    def generate_raw_card(self, private_key, public_key, identity, previous_card_id="", extra_fields=None):
        # type: (VirgilPrivateKey, VirgilPublicKey, str, Optional[str], Optional[dict]) -> RawSignedModel
        """

        Args:
            private_key: PrivateKey for generate self signature.
            public_key: Card Public key.
            identity: Unique identity value.
            previous_card_id: Previous card id that current card is used to override to.
            extra_fields: The additional data associated with the card.

        Returns:
            The instance of newly published Card.
        """
        current_time = Utils.to_timestamp(datetime.datetime.utcnow())
        raw_card = RawSignedModel.generate(self._card_crypto.export_public_key(public_key), identity, current_time, previous_card_id)
        self.model_signer.self_sign(raw_card, private_key, extra_fields=extra_fields)
        return raw_card

    def publish_card(self, *args, **kwargs):
        # type: (...) -> Card
        """
        Publish a new Card using specified params.

        Args:
            *args:
                raw_card: Unpublished raw signed model.
                or
                private_key: PrivateKey for generate self signature.
                public_key: Card Public key.
                identity: Unique identity value.
                previous_card_id: Previous card id that current card is used to override to.
                extra_fields: The additional data associated with the card.
            **kwargs:
                raw_card: Unpublished raw signed model.
                or
                private_key: PrivateKey for generate self signature.
                public_key: Card Public key.
                identity: Unique identity value.
                previous_card_id: Previous card id that current card is used to override to.
                extra_fields: The additional data associated with the card.

        Returns:
            The instance of newly published Card.
        """
        if len(args) == 1 and isinstance(args[0], RawSignedModel):
            return self.__publish_raw_card(*args)
        elif len(kwargs.keys()) == 1 and "raw_card" in kwargs.keys():
            return self.__publish_raw_card(**kwargs)
        else:
            raw_card = self.generate_raw_card(*args, **kwargs)
            raw_published_card = self.__publish_raw_card(raw_card)
            return Card.from_signed_model(self._card_crypto, raw_published_card)

    def get_card(self, card_id):
        # type: (str) -> Card
        """
        Gets the card by specified ID.

        Args:
            card_id: The card ID to be found.

        Returns:
            The instance of found Card
        """
        token_context = TokenContext(None, "get")
        access_token = self._access_token_provider.get_token(token_context)
        raw_card, is_outdated = self.__try_execute(self.card_client.get_card, card_id, access_token, token_context)
        card = Card.from_signed_model(self._card_crypto, raw_card, is_outdated)
        if card.id != card_id:
            raise CardVerificationException("Invalid card")
        self.__validate(card)
        return card

    def search_card(self, identity):
        # type: (Union[str, list]) -> List[Card]
        """
        Searches for cards by specified identity.

        Args:
            identity: The identity (or list of identity) to be found.

        Returns:
            The list of found Card.
        """
        if not identity:
            raise ValueError("Missing identity for search")
        token_context = TokenContext(None, "search")
        access_token = self._access_token_provider.get_token(token_context)
        raw_cards = self.__try_execute(self.card_client.search_card, identity, access_token, token_context)
        cards = list(map(lambda x: Card.from_signed_model(self._card_crypto, x), raw_cards))
        if isinstance(identity, list):
            if any(list(map(lambda x: x.identity not in identity, cards))):

                raise CardVerificationException("Invalid cards")