sentry_sdk.envelope

import io
import json
import mimetypes

from sentry_sdk.session import Session
from sentry_sdk.utils import json_dumps, capture_internal_exceptions

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from typing import Any
    from typing import Optional
    from typing import Union
    from typing import Dict
    from typing import List
    from typing import Iterator

    from sentry_sdk._types import Event, EventDataCategory


def parse_json(data: "Union[bytes, str]") -> "Any":
    # on some python 3 versions this needs to be bytes
    if isinstance(data, bytes):
        data = data.decode("utf-8", "replace")
    return json.loads(data)


class Envelope:
    """
    Represents a Sentry Envelope. The calling code is responsible for adhering to the constraints
    documented in the Sentry docs: https://develop.sentry.dev/sdk/envelopes/#data-model. In particular,
    each envelope may have at most one Item with type "event" or "transaction" (but not both).
    """

    def __init__(
        self,
        headers: "Optional[Dict[str, Any]]" = None,
        items: "Optional[List[Item]]" = None,
    ) -> None:
        if headers is not None:
            headers = dict(headers)
        self.headers = headers or {}
        if items is None:
            items = []
        else:
            items = list(items)
        self.items = items

    @property
    def description(self) -> str:
        return "envelope with %s items (%s)" % (
            len(self.items),
            ", ".join(x.data_category for x in self.items),
        )

    def add_event(
        self,
        event: "Event",
    ) -> None:
        self.add_item(Item(payload=PayloadRef(json=event), type="event"))

    def add_transaction(
        self,
        transaction: "Event",
    ) -> None:
        self.add_item(Item(payload=PayloadRef(json=transaction), type="transaction"))

    def add_profile(
        self,
        profile: "Any",
    ) -> None:
        self.add_item(Item(payload=PayloadRef(json=profile), type="profile"))

    def add_profile_chunk(
        self,
        profile_chunk: "Any",
    ) -> None:
        self.add_item(
            Item(
                payload=PayloadRef(json=profile_chunk),
                type="profile_chunk",
                headers={"platform": profile_chunk.get("platform", "python")},
            )
        )

    def add_checkin(
        self,
        checkin: "Any",
    ) -> None:
        self.add_item(Item(payload=PayloadRef(json=checkin), type="check_in"))

    def add_session(
        self,
        session: "Union[Session, Any]",
    ) -> None:
        if isinstance(session, Session):
            session = session.to_json()
        self.add_item(Item(payload=PayloadRef(json=session), type="session"))

    def add_sessions(
        self,
        sessions: "Any",
    ) -> None:
        self.add_item(Item(payload=PayloadRef(json=sessions), type="sessions"))

    def add_item(
        self,
        item: "Item",
    ) -> None:
        self.items.append(item)

    def get_event(self) -> "Optional[Event]":
        for items in self.items:
            event = items.get_event()
            if event is not None:
                return event
        return None

    def get_transaction_event(self) -> "Optional[Event]":
        for item in self.items:
            event = item.get_transaction_event()
            if event is not None:
                return event
        return None

    def __iter__(self) -> "Iterator[Item]":
        return iter(self.items)

    def serialize_into(
        self,
        f: "Any",
    ) -> None:
        f.write(json_dumps(self.headers))
        f.write(b"\n")
        for item in self.items:
            item.serialize_into(f)

    def serialize(self) -> bytes:
        out = io.BytesIO()
        self.serialize_into(out)
        return out.getvalue()

    @classmethod
    def deserialize_from(
        cls,
        f: "Any",
    ) -> "Envelope":
        headers = parse_json(f.readline())
        items = []
        while 1:
            item = Item.deserialize_from(f)
            if item is None:
                break
            items.append(item)
        return cls(headers=headers, items=items)

    @classmethod
    def deserialize(
        cls,
        bytes: bytes,
    ) -> "Envelope":
        return cls.deserialize_from(io.BytesIO(bytes))

    def __repr__(self) -> str:
        return "<Envelope headers=%r items=%r>" % (self.headers, self.items)