nylas/client/restful_models.py
class Draft(Message):
    attrs = [
        "bcc",
        "cc",
        "body",
        "date",
        "files",
        "from",
        "id",
        "account_id",
        "object",
        "subject",
        "thread_id",
        "to",
        "job_status_id",
        "unread",
        "version",
        "file_ids",
        "reply_to_message_id",
        "reply_to",
        "starred",
        "snippet",
        "tracking",
    ]
    datetime_attrs = {"last_modified_at": "date"}
    collection_name = "drafts"

    def __init__(self, api, thread_id=None):  # pylint: disable=unused-argument
        Message.__init__(self, api)
        NylasAPIObject.__init__(
            self, Thread, api
        )  # pylint: disable=non-parent-init-called
        self.file_ids = []

    def attach(self, file):
        if not file.id:
            file.save()

        self.file_ids.append(file.id)

    def detach(self, file):
        if file.id in self.file_ids:
            self.file_ids.remove(file.id)

    def send(self):
        if not self.id:
            data = self.as_json()
        else:
            data = {"draft_id": self.id}
            if hasattr(self, "version"):
                data["version"] = self.version
            if hasattr(self, "tracking") and self.tracking is not None:
                data["tracking"] = self.tracking

        msg = self.api._create_resource(Send, data)
        if msg:
            return msg

    def delete(self):
        if self.id and self.version is not None:
            data = {"version": self.version}
            self.api._delete_resource(self.cls, self.id, data=data)
nylas/client/restful_models.py
class Folder(NylasAPIObject):
    attrs = ["id", "display_name", "name", "object", "account_id", "job_status_id"]
    collection_name = "folders"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Folder, api)

    @property
    def threads(self):
        return self.child_collection(Thread, folder_id=self.id)

    @property
    def messages(self):
        return self.child_collection(Message, folder_id=self.id)
nylas/client/restful_models.py
class Thread(NylasAPIObject):
    attrs = [
        "draft_ids",
        "id",
        "message_ids",
        "account_id",
        "object",
        "participants",
        "snippet",
        "subject",
        "subject_date",
        "last_message_timestamp",
        "first_message_timestamp",
        "last_message_received_timestamp",
        "last_message_sent_timestamp",
        "unread",
        "starred",
        "version",
        "_folders",
        "_labels",
        "received_recent_date",
        "has_attachments",
    ]
    datetime_attrs = {
        "first_message_at": "first_message_timestamp",
        "last_message_at": "last_message_timestamp",
        "last_message_received_at": "last_message_received_timestamp",
        "last_message_sent_at": "last_message_sent_timestamp",
    }
    datetime_filter_attrs = {
        "last_message_before": "last_message_before",
        "last_message_after": "last_message_after",
        "started_before": "started_before",
        "started_after": "started_after",
    }
    collection_name = "threads"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Thread, api)

    @property
    def messages(self):
        return self.child_collection(Message, thread_id=self.id)

    @property
    def drafts(self):
        return self.child_collection(Draft, thread_id=self.id)

    @property
    def folders(self):
        if self._folders:
            return [Folder.create(self.api, **f) for f in self._folders]
        return []

    @property
    def labels(self):
        if self._labels:
            return [Label.create(self.api, **l) for l in self._labels]
        return []

    def update_folder(self, folder_id):
        update = {"folder": folder_id}
        new_obj = self.api._update_resource(self.cls, self.id, update)
        for attr in self.cls.attrs:
            if hasattr(new_obj, attr):
                setattr(self, attr, getattr(new_obj, attr))
        return self.folder

    def update_labels(self, label_ids=None):
        label_ids = label_ids or []
        update = {"labels": label_ids}
        new_obj = self.api._update_resource(self.cls, self.id, update)
        for attr in self.cls.attrs:
            if hasattr(new_obj, attr):
                setattr(self, attr, getattr(new_obj, attr))
        return self.labels

    def add_labels(self, label_ids=None):
        label_ids = label_ids or []
        labels = [l.id for l in self.labels]
        labels = list(set(labels).union(set(label_ids)))
        return self.update_labels(labels)

    def add_label(self, label_id):
        return self.add_labels([label_id])

    def remove_labels(self, label_ids=None):
        label_ids = label_ids or []
        labels = [l.id for l in self.labels]
        labels = list(set(labels) - set(label_ids))
        return self.update_labels(labels)

    def remove_label(self, label_id):
        return self.remove_labels([label_id])

    def mark_as_seen(self):
        self.mark_as_read()

    def mark_as_read(self):
        update = {"unread": False}
        self.api._update_resource(self.cls, self.id, update)
        self.unread = False

    def mark_as_unread(self):
        update = {"unread": True}
        self.api._update_resource(self.cls, self.id, update)
        self.unread = True

    def star(self):
        update = {"starred": True}
        self.api._update_resource(self.cls, self.id, update)
        self.starred = True

    def unstar(self):
        update = {"starred": False}
        self.api._update_resource(self.cls, self.id, update)
        self.starred = False

    def create_reply(self):
        draft = self.drafts.create()
        draft.thread_id = self.id
        draft.subject = self.subject
        return draft
nylas/client/restful_models.py
class Message(NylasAPIObject):
    attrs = [
        "bcc",
        "body",
        "cc",
        "date",
        "events",
        "files",
        "from",
        "id",
        "account_id",
        "object",
        "snippet",
        "starred",
        "subject",
        "thread_id",
        "job_status_id",
        "to",
        "unread",
        "starred",
        "metadata",
        "_folder",
        "_labels",
        "headers",
        "reply_to",
    ]
    datetime_attrs = {"received_at": "date"}
    datetime_filter_attrs = {
        "received_before": "received_before",
        "received_after": "received_after",
    }
    collection_name = "messages"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Message, api)

    @property
    def attachments(self):
        return self.child_collection(File, message_id=self.id)

    @property
    def folder(self):
        # Instantiate a Folder object from the API response
        if self._folder:
            return Folder.create(self.api, **self._folder)

    @property
    def labels(self):
        if self._labels:
            return [Label.create(self.api, **l) for l in self._labels]
        return []

    def update_folder(self, folder_id):
        update = {"folder": folder_id}
        new_obj = self.api._update_resource(self.cls, self.id, update)
        for attr in self.cls.attrs:
            if hasattr(new_obj, attr):
                setattr(self, attr, getattr(new_obj, attr))
        return self.folder

    def update_labels(self, label_ids=None):
        label_ids = label_ids or []
        update = {"labels": label_ids}
        new_obj = self.api._update_resource(self.cls, self.id, update)
        for attr in self.cls.attrs:
            if hasattr(new_obj, attr):
                setattr(self, attr, getattr(new_obj, attr))
        return self.labels

    def add_labels(self, label_ids=None):
        label_ids = label_ids or []
        labels = [l.id for l in self.labels]
        labels = list(set(labels).union(set(label_ids)))
        return self.update_labels(labels)

    def add_label(self, label_id):
        return self.add_labels([label_id])

    def remove_labels(self, label_ids=None):
        label_ids = label_ids or []
        labels = [l.id for l in self.labels]
        labels = list(set(labels) - set(label_ids))
        return self.update_labels(labels)

    def remove_label(self, label_id):
        return self.remove_labels([label_id])

    def mark_as_seen(self):
        self.mark_as_read()

    def mark_as_read(self):
        update = {"unread": False}
        self.api._update_resource(self.cls, self.id, update)
        self.unread = False

    def mark_as_unread(self):
        update = {"unread": True}
        self.api._update_resource(self.cls, self.id, update)
        self.unread = True

    def star(self):
        update = {"starred": True}
        self.api._update_resource(self.cls, self.id, update)
        self.starred = True

    def unstar(self):
        update = {"starred": False}
        self.api._update_resource(self.cls, self.id, update)
        self.starred = False

    @property
    def raw(self):
        headers = {"Accept": "message/rfc822"}
        response = self.api._get_resource_raw(Message, self.id, headers=headers)
        if response.status_code == 202:
            raise UnSyncedError(response.content)
        return response.content
nylas/client/restful_models.py
class Event(NylasAPIObject):
    attrs = [
        "id",
        "account_id",
        "title",
        "description",
        "conferencing",
        "location",
        "read_only",
        "when",
        "busy",
        "participants",
        "calendar_id",
        "recurrence",
        "status",
        "master_event_id",
        "job_status_id",
        "owner",
        "original_start_time",
        "object",
        "message_id",
        "ical_uid",
        "metadata",
        "notifications",
    ]
    datetime_attrs = {"original_start_at": "original_start_time"}
    collection_name = "events"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Event, api)

    def as_json(self):
        dct = NylasAPIObject.as_json(self)
        # Filter some parameters we got from the API
        if dct.get("when"):
            # Currently, the event (self) and the dict (dct) share the same
            # reference to the `'when'` dict.  We need to clone the dict so
            # that when we remove the object key, the original event's
            # `'when'` reference is unmodified.
            dct["when"] = dct["when"].copy()
            dct["when"].pop("object", None)

        if dct.get("participants") and isinstance(dct.get("participants"), list):
            # The status of a participant cannot be updated and, if the key is
            # included, it will return an error from the API
            for participant in dct.get("participants"):
                participant.pop("status", None)

        return dct

    def rsvp(self, status, comment=None):
        if not self.message_id:
            raise ValueError(
                "This event was not imported from an iCalendar invite, and so it is not possible to RSVP via Nylas"
            )
        if status not in {"yes", "no", "maybe"}:
            raise ValueError("invalid status: {status}".format(status=status))

        url = "{api_server}/send-rsvp".format(api_server=self.api.api_server)
        data = {
            "event_id": self.id,
            "status": status,
            "comment": comment,
        }
        response = self.api.session.post(url, json=data)
        if response.status_code >= 400:
            raise NylasApiError(response)
        result = response.json()
        return Event.create(self, **result)

    def generate_ics(self, ical_uid=None, method=None, prodid=None):
        """
        Generate an ICS file server-side, from an Event

        Args:
            ical_uid (str): Unique identifier used events across calendaring systems
            method (str): Description of invitation and response methods for attendees
            prodid (str): Company-specific unique product identifier

        Returns:
            str: String for writing directly into an ICS file

        Raises:
            ValueError: If the event does not have calendar_id or when set
            RuntimeError: If the server returns an object without an ics string
        """
        if not self.calendar_id or not self.when:
            raise ValueError(
                "Cannot generate an ICS file for an event without a Calendar ID or when set"
            )

        payload = {}
        ics_options = {}
        if self.id:
            payload["event_id"] = self.id
        else:
            payload = self.as_json()

        if ical_uid:
            ics_options["ical_uid"] = ical_uid
        if method:
            ics_options["method"] = method
        if prodid:
            ics_options["prodid"] = prodid

        if ics_options:
            payload["ics_options"] = ics_options

        response = self.api._post_resource(Event, None, "to-ics", payload)
        if "ics" in response:
            return response["ics"]
        raise RuntimeError(
            "Unexpected response from the API server. Returned 200 but no 'ics' string found."
        )

    def save(self, **kwargs):
        if (
            self.conferencing
            and "details" in self.conferencing
            and "autocreate" in self.conferencing
        ):
            raise ValueError(
                "Cannot set both 'details' and 'autocreate' in conferencing object."
            )

        super(Event, self).save(**kwargs)
nylas/client/restful_models.py
class Contact(NylasAPIObject):
    attrs = [
        "id",
        "object",
        "account_id",
        "given_name",
        "middle_name",
        "surname",
        "suffix",
        "nickname",
        "company_name",
        "job_title",
        "job_status_id",
        "manager_name",
        "office_location",
        "source",
        "notes",
        "picture_url",
    ]
    date_attrs = {"birthday": "birthday"}
    typed_dict_attrs = {
        "emails": "email",
        "im_addresses": "im_address",
        "physical_addresses": None,
        "phone_numbers": "number",
        "web_pages": "url",
    }
    collection_name = "contacts"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Contact, api)

    def get_picture(self):
        if not self.get("picture_url", None):
            return None

        response = self.api._get_resource_raw(
            Contact, self.id, extra="picture", stream=True
        )
        if response.status_code >= 400:
            raise NylasApiError(response)
        return response.raw
nylas/client/restful_models.py
class Label(NylasAPIObject):
    attrs = ["id", "display_name", "name", "object", "account_id", "job_status_id"]
    collection_name = "labels"

    def __init__(self, api):
        NylasAPIObject.__init__(self, Label, api)

    @property
    def threads(self):
        return self.child_collection(Thread, label_id=self.id)

    @property
    def messages(self):
        return self.child_collection(Message, label_id=self.id)
nylas/client/restful_models.py
class File(NylasAPIObject):
    attrs = [
        "content_type",
        "filename",
        "id",
        "content_id",
        "account_id",
        "object",
        "size",
        "message_ids",
    ]
    collection_name = "files"

    def save(self):  # pylint: disable=arguments-differ
        stream = getattr(self, "stream", None)
        if not stream:
            data = getattr(self, "data", None)
            if data:
                stream = StringIO(data)

        if not stream:
            message = (
                "File object not properly formatted, "
                "must provide either a stream or data."
            )
            raise FileUploadError(message)

        file_info = (self.filename, stream, self.content_type, {})  # upload headers

        new_obj = self.api._create_resources(File, {"file": file_info})
        new_obj = new_obj[0]
        for attr in self.attrs:
            if hasattr(new_obj, attr):
                setattr(self, attr, getattr(new_obj, attr))

    def download(self):
        if not self.id:
            message = "Can't download a file that hasn't been uploaded."
            raise FileUploadError(message)

        return self.api._get_resource_data(File, self.id, extra="download")

    def __init__(self, api):
        NylasAPIObject.__init__(self, File, api)
nylas/client/delta_models.py
class Delta(RestfulModel):
    attrs = (
        "id",
        "cursor",
        "event",
        "object",
        "_attributes",
    )
    read_only_attrs = tuple(attrs)
    class_mapping = {
        "contact": Contact,
        "file": File,
        "message": Message,
        "draft": Draft,
        "thread": Thread,
        "event": Event,
        "folder": Folder,
        "label": Label,
    }

    def __init__(self, api):
        RestfulModel.__init__(self, Delta, api)

    @property
    def attributes(self):
        """
        Instantiate the object provided in the Delta

        Returns:
            NylasAPIObject: The object of NylasAPIObject type represented in the Delta
        """
        if self._attributes and self.object and self.object in self.class_mapping:
            return self.class_mapping[self.object].create(self.api, **self._attributes)
nylas/client/delta_models.py
class Deltas(RestfulModel):
    attrs = (
        "cursor_start",
        "cursor_end",
        "_deltas",
    )
    read_only_attrs = tuple(attrs)

    def __init__(self, api):
        RestfulModel.__init__(self, Deltas, api)

    @property
    def deltas(self):
        """
        Instantiate a Delta object from the API response

        Returns:
            list[Delta]: List of Delta instantiated objects
        """
        if self._deltas:
            deltas = []
            for delta in self._deltas:
                deltas.append(Delta.create(self.api, **delta))
            return deltas


Question: import pytest
from urlobject import URLObject
from nylas.client.delta_models import Deltas, Delta
from nylas.client.restful_models import (
    Contact,
    File,
    Message,
    Draft,
    Thread,
    Event,
    Folder,
    Label,
)



@pytest.mark.usefixtures("mock_deltas_since")
def test_deltas_since(mocked_responses, api_client):
    deltas = api_client.deltas.since("cursor")
    request = mocked_responses.calls[0].request
    assert URLObject(request.url).path == "/delta"
    assert URLObject(request.url).query_dict == {"cursor": "cursor"}
    assert request.method == "GET"
    assert isinstance(deltas, Deltas)
    assert deltas.cursor_start == "start_cursor"
    assert deltas.cursor_end == "end_cursor"
    assert len(deltas.deltas) == 8
    assert isinstance(deltas.deltas[0].attributes, Contact)
    assert deltas.deltas[0].cursor == "contact_cursor"
    assert deltas.deltas[0].event == "create"
    assert deltas.deltas[0].id == "delta-1"
    assert deltas.deltas[0].object == "contact"
    assert isinstance(deltas.deltas[1].attributes, File)
    assert deltas.deltas[1].cursor == "file_cursor"
    assert deltas.deltas[1].event == "create"
    assert deltas.deltas[1].id == "delta-2"
    assert deltas.deltas[1].object == "file"
    assert isinstance(deltas.deltas[2].attributes, Message)
    assert deltas.deltas[2].cursor == "message_cursor"
    assert deltas.deltas[2].event == "create"
    assert deltas.deltas[2].id == "delta-3"
    assert deltas.deltas[2].object == "message"
    assert isinstance(deltas.deltas[3].attributes, Draft)
    assert deltas.deltas[3].cursor == "draft_cursor"
    assert deltas.deltas[3].event == "create"
    assert deltas.deltas[3].id == "delta-4"
    assert deltas.deltas[3].object == "draft"

Answer:     assert isinstance(deltas.deltas[4].attributes, Thread)