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/scheduler_restful_model_collection.py
class SchedulerRestfulModelCollection(RestfulModelCollection):
    def __init__(self, api):
        # Make a copy of the API as we need to change the base url for Scheduler calls
        scheduler_api = copy.copy(api)
        scheduler_api.api_server = "https://api.schedule.nylas.com"
        RestfulModelCollection.__init__(self, Scheduler, scheduler_api)

    def get_google_availability(self):
        return self._execute_provider_availability("google")

    def get_office_365_availability(self):
        return self._execute_provider_availability("o365")

    def get_page_slug(self, slug):
        page_response = self.api._get_resource_raw(
            self.model_class, slug, extra="info", path="schedule"
        ).json()
        return Scheduler.create(self.api, **page_response)

    def get_available_time_slots(self, slug):
        response = self.api._get_resource_raw(
            self.model_class, slug, extra="timeslots", path="schedule"
        ).json()
        return [
            SchedulerTimeSlot.create(self.api, **x) for x in response if x is not None
        ]

    def book_time_slot(self, slug, timeslot):
        response = self.api._post_resource(
            self.model_class, slug, "timeslots", timeslot.as_json(), path="schedule"
        )
        return SchedulerBookingConfirmation.create(self.api, **response)

    def cancel_booking(self, slug, edit_hash, reason):
        return self.api._post_resource(
            self.model_class,
            slug,
            "{}/cancel".format(edit_hash),
            {"reason": reason},
            path="schedule",
        )

    def confirm_booking(self, slug, edit_hash):
        booking_response = self.api._post_resource(
            self.model_class, slug, "{}/confirm".format(edit_hash), {}, path="schedule"
        )
        return SchedulerBookingConfirmation.create(self.api, **booking_response)

    def _execute_provider_availability(self, provider):
        return self.api._get_resource_raw(
            self.model_class,
            None,
            extra="availability/{}".format(provider),
            path="schedule",
        ).json()
nylas/client/restful_models.py
class Component(NylasAPIObject):
    attrs = [
        "id",
        "account_id",
        "name",
        "type",
        "action",
        "active",
        "settings",
        "public_account_id",
        "public_token_id",
        "public_application_id",
        "access_token",
        "allowed_domains",
    ]
    datetime_attrs = {
        "created_at": "created_at",
        "updated_at": "updated_at",
    }
    read_only_attrs = {"id", "public_application_id", "created_at", "updated_at"}

    collection_name = None
    api_root = "component"

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

    def as_json(self):
        dct = NylasAPIObject.as_json(self)
        # "type" cannot be modified after created
        if self.id:
            dct.pop("type")
        return dct
nylas/client/restful_models.py
class Webhook(NylasAPIObject):
    attrs = (
        "id",
        "callback_url",
        "state",
        "triggers",
        "application_id",
        "version",
    )
    read_only_attrs = {"id", "application_id", "version"}

    collection_name = "webhooks"
    api_root = "a"

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

    def as_json(self):
        dct = {}
        # Only 'state' can get updated
        if self.id:
            dct["state"] = self.state
        else:
            dct = NylasAPIObject.as_json(self)
        return dct

    class Trigger(str, Enum):
        """
        This is an Enum representing all the possible webhook triggers

        see more: https://developer.nylas.com/docs/developer-tools/webhooks/available-webhooks
        """

        ACCOUNT_CONNECTED = "account.connected"
        ACCOUNT_RUNNING = "account.running"
        ACCOUNT_STOPPED = "account.stopped"
        ACCOUNT_INVALID = "account.invalid"
        ACCOUNT_SYNC_ERROR = "account.sync_error"
        MESSAGE_CREATED = "message.created"
        MESSAGE_OPENED = "message.opened"
        MESSAGE_UPDATED = "message.updated"
        MESSAGE_LINK_CLICKED = "message.link_clicked"
        THREAD_REPLIED = "thread.replied"
        CONTACT_CREATED = "contact.created"
        CONTACT_UPDATED = "contact.updated"
        CONTACT_DELETED = "contact.deleted"
        CALENDAR_CREATED = "calendar.created"
        CALENDAR_UPDATED = "calendar.updated"
        CALENDAR_DELETED = "calendar.deleted"
        EVENT_CREATED = "event.created"
        EVENT_UPDATED = "event.updated"
        EVENT_DELETED = "event.deleted"
        JOB_SUCCESSFUL = "job.successful"
        JOB_FAILED = "job.failed"

    class State(str, Enum):
        """
        This is an Enum representing all the possible webhook states

        see more: https://developer.nylas.com/docs/developer-tools/webhooks/#enable-and-disable-webhooks
        """

        ACTIVE = "active"
        INACTIVE = "inactive"
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/errors.py
class MessageRejectedError(NylasError):
    pass
nylas/client/outbox_models.py
class Outbox:
    def __init__(self, api):
        self.api = api

    def send(self, draft, send_at, retry_limit_datetime=None):
        """
        Send a message via Outbox

        Args:
            draft (Draft | OutboxMessage): The message to send
            send_at (datetime | int): The date and time to send the message. If set to 0, Outbox will send this message immediately.
            retry_limit_datetime (datetime | int): Optional date and time to stop retry attempts for a message.

        Returns:
            OutboxJobStatus: The Outbox message job status

        Raises:
            ValueError: If the date and times provided are not valid
        """
        draft_json = draft.as_json()
        send_at, retry_limit_datetime = self._validate_and_format_datetime(
            send_at, retry_limit_datetime
        )

        draft_json["send_at"] = send_at
        if retry_limit_datetime is not None:
            draft_json["retry_limit_datetime"] = retry_limit_datetime

        return self.api._create_resource(OutboxJobStatus, draft_json)

    def update(
        self, job_status_id, draft=None, send_at=None, retry_limit_datetime=None
    ):
        """
        Update a scheduled Outbox message

        Args:
            job_status_id (str): The ID of the outbox job status
            draft (Draft | OutboxMessage): The message object with updated values
            send_at (datetime | int): The date and time to send the message. If set to 0, Outbox will send this message immediately.
            retry_limit_datetime (datetime | int): Optional date and time to stop retry attempts for a message.

        Returns:
            OutboxJobStatus: The updated Outbox message job status

        Raises:
            ValueError: If the date and times provided are not valid
        """
        payload = {}
        if draft:
            payload = draft.as_json()
        send_at, retry_limit_datetime = self._validate_and_format_datetime(
            send_at, retry_limit_datetime
        )

        if send_at is not None:
            payload["send_at"] = send_at
        if retry_limit_datetime is not None:
            payload["retry_limit_datetime"] = retry_limit_datetime

        response = self.api._patch_resource(OutboxJobStatus, job_status_id, payload)
        return OutboxJobStatus.create(self.api, **response)

    def delete(self, job_status_id):
        """
        Delete a scheduled Outbox message

        Args:
            job_status_id (str): The ID of the outbox job status to delete
        """

        self.api._delete_resource(OutboxJobStatus, job_status_id)

    def send_grid_verification_status(self):
        """
        SendGrid - Check Authentication and Verification Status

        Returns:
            SendGridVerifiedStatus: The status of the domain authentication and the single sender verification for SendGrid integrations

        Raises:
            RuntimeError: If the server returns an object without results
        """
        response = self.api._get_resource_raw(
            SendGridVerifiedStatus, None, extra="verified_status"
        )
        response_body = response.json()
        if "results" not in response_body:
            raise RuntimeError(
                "Unexpected response from the API server. Returned 200 but no 'ics' string found."
            )
        return SendGridVerifiedStatus.create(self.api, **response_body["results"])

    def delete_send_grid_sub_user(self, email_address):
        """
        SendGrid -  Delete SendGrid Subuser and UAS Grant

        Args:
            email_address (str): Email address for SendGrid subuser to delete
        """
        payload = {"email": email_address}

        self.api._delete_resource(SendGridVerifiedStatus, "subuser", data=payload)

    def _validate_and_format_datetime(self, send_at, retry_limit_datetime):
        send_at_epoch = (
            timestamp_from_dt(send_at) if isinstance(send_at, datetime) else send_at
        )
        retry_limit_datetime_epoch = (
            timestamp_from_dt(retry_limit_datetime)
            if isinstance(retry_limit_datetime, datetime)
            else retry_limit_datetime
        )
        now_epoch = timestamp_from_dt(datetime.today())

        if send_at_epoch and send_at_epoch != 0 and send_at_epoch < now_epoch:
            raise ValueError(
                "Cannot set message to be sent at a time before the current time."
            )

        if retry_limit_datetime_epoch and retry_limit_datetime_epoch != 0:
            current_send_at = (
                send_at_epoch if send_at_epoch and send_at_epoch != 0 else now_epoch
            )
            if retry_limit_datetime_epoch < current_send_at:
                raise ValueError(
                    "Cannot set message to stop retrying before time to send at."
                )

        return send_at_epoch, retry_limit_datetime_epoch
nylas/utils.py
def create_request_body(data, datetime_attrs):
    """
    Given a dictionary of data, and a dictionary of datetime attributes,
    return a new dictionary that is suitable for a request. It converts
    any datetime attributes that may be present to their timestamped
    equivalent, and it filters out any attributes set to "None".
    """
    if not data:
        return data

    new_data = {}
    for key, value in data.items():
        if key in datetime_attrs and isinstance(value, datetime):
            new_key = datetime_attrs[key]
            new_data[new_key] = timestamp_from_dt(value)
        elif value is not None:
            new_data[key] = value

    return new_data
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/neural_api_models.py
class Neural(RestfulModel):
    def __init__(self, api):
        RestfulModel.__init__(self, Neural, api)

    def sentiment_analysis_message(self, message_ids):
        body = {"message_id": message_ids}
        return self.api._request_neural_resource(NeuralSentimentAnalysis, body)

    def sentiment_analysis_text(self, text):
        body = {"text": text}
        return self.api._request_neural_resource(NeuralSentimentAnalysis, body)

    def extract_signature(self, message_ids, parse_contacts=None, options=None):
        body = {"message_id": message_ids}
        if parse_contacts is not None and isinstance(parse_contacts, bool):
            body["parse_contacts"] = parse_contacts
        if options is not None and isinstance(options, NeuralMessageOptions):
            _add_options_to_body(body, options)
        signatures = self.api._request_neural_resource(NeuralSignatureExtraction, body)
        if parse_contacts is not False:
            for sig in signatures:
                sig.contacts = NeuralSignatureContact.create(self.api, **sig.contacts)
        return signatures

    def ocr_request(self, file_id, pages=None):
        body = {"file_id": file_id}
        if pages is not None and isinstance(pages, list):
            body["pages"] = pages
        return self.api._request_neural_resource(NeuralOcr, body)

    def categorize(self, message_ids):
        body = {"message_id": message_ids}
        categorized = self.api._request_neural_resource(NeuralCategorizer, body)
        for message in categorized:
            message.categorizer = Categorize.create(self.api, **message.categorizer)
        return categorized

    def clean_conversation(self, message_ids, options=None):
        body = {"message_id": message_ids}
        if options is not None and isinstance(options, NeuralMessageOptions):
            _add_options_to_body(body, options)
        return self.api._request_neural_resource(NeuralCleanConversation, body)
nylas/client/restful_models.py
class RoomResource(NylasAPIObject):
    attrs = [
        "object",
        "email",
        "name",
        "capacity",
        "building",
        "floor_name",
        "floor_number",
    ]
    object_type = "room_resource"
    collection_name = "resources"

    def __init__(self, api):
        NylasAPIObject.__init__(self, RoomResource, api)
nylas/client/restful_model_collection.py
class RestfulModelCollection(object):
    def __init__(self, cls, api, filter=None, offset=0, **filters):
        if filter:
            filters.update(filter)
        from nylas.client import APIClient

        if not isinstance(api, APIClient):
            raise Exception("Provided api was not an APIClient.")

        filters.setdefault("offset", offset)

        self.model_class = cls
        self.filters = filters
        self.api = api

    def __iter__(self):
        return self.values()

    def values(self):
        limit = self.filters.get("limit")
        offset = self.filters["offset"]
        fetched = 0
        # Currently, the Nylas API handles pagination poorly: API responses do not expose
        # any information about pagination, so the client does not know whether there is
        # another page of data or not. For example, if the client sends an API request
        # without a limit specified, and the response contains 100 items, how can it tell
        # if there are 100 items in total, or if there more items to fetch on the next page?
        # It can't! The only way to know is to ask for the next page (by repeating the API
        # request with `offset=100`), and see if you get more items or not.
        # If it does not receive more items, it can assume that it has retrieved all the data.
        while True:
            if limit:
                if fetched >= limit:
                    break

                req_limit = min(CHUNK_SIZE, limit - fetched)
            else:
                req_limit = CHUNK_SIZE

            models = self._get_model_collection(offset + fetched, req_limit)
            if not models:
                break

            for model in models:
                yield model

            fetched += len(models)

    def first(self):
        results = self._get_model_collection(0, 1)
        if results:
            return results[0]
        return None

    def all(self, limit=float("infinity")):
        if "limit" in self.filters and self.filters["limit"] is not None:
            limit = self.filters["limit"]
        return self._range(self.filters["offset"], limit)

    def where(self, filter=None, **filters):
        # Some API parameters like "from" and "in" also are
        # Python reserved keywords. To work around this, we rename
        # them to "from_" and "in_". The API still needs them in
        # their correct form though.
        reserved_keywords = ["from", "in"]
        for keyword in reserved_keywords:
            escaped_keyword = "{}_".format(keyword)
            if escaped_keyword in filters:
                filters[keyword] = filters.get(escaped_keyword)
                del filters[escaped_keyword]

        if filter:
            filters.update(filter)
        filters.setdefault("offset", 0)

        if "metadata_pair" in filters:
            pairs = convert_metadata_pairs_to_array(filters["metadata_pair"])
            filters["metadata_pair"] = pairs

        collection = copy(self)
        collection.filters = filters
        return collection

    def get(self, id):
        return self._get_model(id)

    def create(self, **kwargs):
        return self.model_class.create(self.api, **kwargs)

    def delete(self, id, data=None, **kwargs):
        return self.api._delete_resource(self.model_class, id, data=data, **kwargs)

    def search(self, q):  # pylint: disable=invalid-name
        from nylas.client.restful_models import (
            Message,
            Thread,
        )  # pylint: disable=cyclic-import

        if self.model_class is Thread or self.model_class is Message:
            kwargs = {"q": q}
            return self.api._get_resources(self.model_class, extra="search", **kwargs)
        else:
            raise Exception("Searching is only allowed on Thread and Message models")

    def __getitem__(self, key):
        if isinstance(key, slice):
            if key.step is not None:
                raise ValueError(
                    "'step' not supported for slicing "
                    "RestfulModelCollection objects "
                    "(e.g. messages[::step])"
                )
            elif key.start < 0 or key.stop < 0:
                raise ValueError("slice indices must be positive")
            elif key.stop - key.start < 0:
                raise ValueError(
                    "ending slice index cannot be less than " "starting index"
                )
            return self._range(key.start, key.stop - key.start)
        else:
            return self._get_model_collection(key, 1)[0]

    # Private functions

    def _get_model_collection(self, offset=0, limit=CHUNK_SIZE):
        filters = copy(self.filters)
        filters["offset"] = offset
        if not filters.get("limit"):
            filters["limit"] = limit

        return self.api._get_resources(self.model_class, **filters)

    def _get_model(self, id):
        return self.api._get_resource(self.model_class, id, **self.filters)

    def _range(self, offset=0, limit=CHUNK_SIZE):
        accumulated = []
        while len(accumulated) < limit:
            to_fetch = min(limit - len(accumulated), CHUNK_SIZE)
            results = self._get_model_collection(offset + len(accumulated), to_fetch)
            accumulated.extend(results)

            # done if we run out of data to fetch
            if not results or len(results) < to_fetch:
                break

        return accumulated
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 Account(NylasAPIObject):
    api_root = "a"

    attrs = [
        "account_id",
        "billing_state",
        "email",
        "id",
        "namespace_id",
        "provider",
        "sync_state",
        "trial",
        "metadata",
    ]

    collection_name = "accounts"

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

    def as_json(self):
        dct = {"metadata": self.metadata}
        return dct

    def upgrade(self):
        return self.api._call_resource_method(self, self.account_id, "upgrade", None)

    def downgrade(self):
        return self.api._call_resource_method(self, self.account_id, "downgrade", None)
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 JobStatus(NylasAPIObject):
    attrs = [
        "id",
        "account_id",
        "job_status_id",
        "action",
        "object",
        "status",
        "original_data",
    ]
    datetime_attrs = {"created_at": "created_at"}
    collection_name = "job-statuses"

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

    def is_successful(self):
        return self.status == "successful"
nylas/client/delta_collection.py
class DeltaCollection:
    path = "delta"

    def __init__(self, api):
        self.api = api

    def latest_cursor(self):
        """
        Returns the latest delta cursor

        Returns:
            str: The latest cursor

        Raises:
            RuntimeError: If the server returns an object without a cursor
        """

        response = self.api._post_resource(
            Delta, "latest_cursor", None, None, path=self.path
        )
        if "cursor" not in response:
            raise RuntimeError(
                "Unexpected response from the API server. Returned 200 but no 'cursor' string found."
            )

        return response["cursor"]

    def since(self, cursor, view=None, include_types=None, excluded_types=None):
        """
        Get a list of delta cursors since a specified cursor

        Args:
            cursor (str): The first cursor to request from
            view (str): Value representing if delta expands thread and message objects.
            include_types (list[str] | str): The objects to exclusively include in the returned deltas. Note you cannot set both included and excluded types.
            excluded_types (list[str] | str): The objects to exclude in the returned deltas. Note you cannot set both included and excluded types.

        Returns:
            Deltas: The API response containing the list of deltas

        Raises:
            ValueError: If both include_types and excluded_types are set
        """

        include_types, excluded_types = _validate_types(include_types, excluded_types)
        response = self.api._get_resource_raw(
            Delta,
            None,
            path=self.path,
            cursor=cursor,
            view=view,
            include_types=include_types,
            excluded_types=excluded_types,
        ).json()
        return Deltas.create(self.api, **response)

    def stream(
        self,
        cursor,
        callback=None,
        timeout=None,
        view=None,
        include_types=None,
        excluded_types=None,
    ):
        """
        Stream deltas

        Args:
            cursor (str): The cursor to stream from
            callback: A callable function to invoke on each delta received. No callback is set by default.
            timeout (int): The number of seconds to stream for before timing out. No timeout is set by default.
            view (str): Value representing if delta expands thread and message objects.
            include_types (list[str] | str): The objects to exclusively include in the returned deltas. Note you cannot set both included and excluded types.
            excluded_types (list[str] | str): The objects to exclude in the returned deltas. Note you cannot set both included and excluded types.

        Returns:
            list[Delta]: The list of streamed deltas

        Raises:
            ValueError: If both include_types and excluded_types are set
        """

        deltas = []
        include_types, excluded_types = _validate_types(include_types, excluded_types)
        emit_deltas = False
        if callback and callable(callback):
            emit_deltas = True

        try:
            response = self.api._get_resource_raw(
                Delta,
                "streaming",
                stream=True,
                path=self.path,
                stream_timeout=timeout,
                cursor=cursor,
                view=view,
                include_types=include_types,
                excluded_types=excluded_types,
            )
            for raw_rsp in response.iter_lines():
                if raw_rsp:
                    response_json = json.loads(raw_rsp)
                    delta = Delta.create(self.api, **response_json)
                    deltas.append(delta)
                    if emit_deltas:
                        callback(delta)
        except ReadTimeout:
            pass

        return deltas

    def longpoll(
        self,
        cursor,
        timeout,
        callback=None,
        view=None,
        include_types=None,
        excluded_types=None,
    ):
        """
        Long-poll for deltas

        Args:
            cursor (str): The cursor to poll from
            timeout (int): The number of seconds to poll for before timing out
            callback: A callable function to invoke on each delta received. No callback is set by default.
            view (str): Value representing if delta expands thread and message objects.
            include_types (list[str] | str): The objects to exclusively include in the returned deltas. Note you cannot set both included and excluded types.
            excluded_types (list[str] | str): The objects to exclude in the returned deltas. Note you cannot set both included and excluded types.

        Returns:
            Deltas: The API response containing the list of deltas

        Raises:
            ValueError: If both include_types and excluded_types are set
        """

        delta = {}
        include_types, excluded_types = _validate_types(include_types, excluded_types)
        emit_deltas = False
        if callback and callable(callback):
            emit_deltas = True

        buffer = bytearray()
        response = self.api._get_resource_raw(
            Delta,
            "longpoll",
            stream=True,
            path=self.path,
            timeout=timeout,
            cursor=cursor,
            view=view,
            include_types=include_types,
            excluded_types=excluded_types,
        )
        for raw_rsp in response.iter_lines():
            if raw_rsp:
                buffer.extend(raw_rsp)
                try:
                    buffer_json = json.loads(buffer.decode())
                    delta = Deltas.create(self.api, **buffer_json)
                    if emit_deltas:
                        callback(delta)
                except ValueError:
                    continue

        return delta
nylas/client/restful_models.py
class APIAccount(NylasAPIObject):
    attrs = [
        "account_id",
        "email_address",
        "id",
        "name",
        "object",
        "organization_unit",
        "provider",
        "sync_state",
    ]
    datetime_attrs = {"linked_at": "linked_at"}

    collection_name = "accounts"

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

    def as_json(self):
        dct = NylasAPIObject.as_json(self)
        return dct
nylas/_client_sdk_version.py
__VERSION__ = "5.6.0"
nylas/utils.py
def timestamp_from_dt(dt, epoch=datetime(1970, 1, 1)):
    """
    Convert a datetime to a timestamp.
    https://stackoverflow.com/a/8778548/141395
    """
    # For offset-aware datetime objects, convert them first before performing delta
    if dt.tzinfo is not None and dt.utcoffset() is not None:
        dt = dt.replace(tzinfo=None) - dt.utcoffset()

    delta = dt - epoch

    return int(delta.total_seconds() / timedelta(seconds=1).total_seconds())
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 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 SingletonAccount(APIAccount):
    # This is an APIAccount that lives under /account.
    collection_name = "account"
nylas/client/errors.py
class NylasApiError(HTTPError):
    """
    Error class for Nylas API Errors
    This class provides more information to the user sent from the server, if present
    """

    def __init__(self, response):
        try:
            response_json = json.loads(response.text)
            error_message = "%s %s. Reason: %s. Nylas Error Type: %s" % (
                response.status_code,
                response.reason,
                response_json["message"],
                response_json["type"],
            )
            super(NylasApiError, self).__init__(error_message, response=response)
        except (ValueError, KeyError):
            super(NylasApiError, self).__init__(response.text, response=response)
nylas/client/restful_models.py
class Calendar(NylasAPIObject):
    attrs = [
        "id",
        "account_id",
        "name",
        "description",
        "job_status_id",
        "metadata",
        "read_only",
        "is_primary",
        "object",
    ]
    collection_name = "calendars"

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

    @property
    def events(self):
        return self.child_collection(Event, calendar_id=self.id)


Question: import sys
import json
import requests
import six
from os import environ
from base64 import b64encode
from datetime import datetime, timedelta
from itertools import chain
from urlobject import URLObject
from six.moves.urllib.parse import urlencode
from nylas._client_sdk_version import __VERSION__
from nylas.client.delta_collection import DeltaCollection
from nylas.client.errors import MessageRejectedError, NylasApiError
from nylas.client.outbox_models import Outbox
from nylas.client.restful_model_collection import RestfulModelCollection
from nylas.client.restful_models import (
    Calendar,
    Contact,
    Event,
    RoomResource,
    Message,
    Thread,
    File,
    Account,
    APIAccount,
    SingletonAccount,
    Folder,
    Label,
    Draft,
    Component,
    JobStatus,
    Webhook,
)
from nylas.client.neural_api_models import Neural
from nylas.client.scheduler_restful_model_collection import (
    SchedulerRestfulModelCollection,
)
from nylas.utils import timestamp_from_dt, create_request_body
        self,
        emails,
        duration,
        interval,
        start_at,
        end_at,
        buffer=None,
        free_busy=None,
        open_hours=None,
    ):
        if isinstance(emails, six.string_types):
            emails = [[emails]]
        elif isinstance(emails[0], list) is False:
            raise ValueError("'emails' must be a list of lists.")
        if isinstance(duration, timedelta):
            duration_minutes = int(duration.total_seconds() // 60)
        else:
            duration_minutes = int(duration)
        if isinstance(interval, timedelta):
            interval_minutes = int(interval.total_seconds() // 60)
        else:
            interval_minutes = int(interval)
        if isinstance(start_at, datetime):
            start_time = timestamp_from_dt(start_at)
        else:
            start_time = start_at
        if isinstance(end_at, datetime):
            end_time = timestamp_from_dt(end_at)
        else:
            end_time = end_at
        if open_hours is not None:
            self._validate_open_hours(emails, open_hours, free_busy)

        url = "{api_server}/calendars/availability/consecutive".format(
            api_server=self.api_server
        )
        data = {
            "emails": emails,
            "duration_minutes": duration_minutes,
            "interval_minutes": interval_minutes,
            "start_time": start_time,
            "end_time": end_time,
            "free_busy": free_busy or [],
            "open_hours": open_hours or [],
        }
        if buffer is not None:
            data["buffer"] = buffer

        resp = self.session.post(url, json=data)
        _validate(resp)
        return resp.json()

    @property
    def account(self):
        return self._get_resource(SingletonAccount, "")

    @property
    def accounts(self):
        if self.is_opensource_api():
            return RestfulModelCollection(APIAccount, self)
        return RestfulModelCollection(Account, self)

    @property
    def threads(self):
        return RestfulModelCollection(Thread, self)

    @property
    def folders(self):
        return RestfulModelCollection(Folder, self)

    @property
    def labels(self):
        return RestfulModelCollection(Label, self)

    @property
    def messages(self):
        return RestfulModelCollection(Message, self)

    @property
    def files(self):
        return RestfulModelCollection(File, self)

    @property
    def drafts(self):
        return RestfulModelCollection(Draft, self)

    @property
    def contacts(self):
        return RestfulModelCollection(Contact, self)

    @property
    def events(self):
        return RestfulModelCollection(Event, self)

    @property
    def room_resources(self):
        return RestfulModelCollection(RoomResource, self)

    @property
    def calendars(self):
        return RestfulModelCollection(Calendar, self)

    @property
    def job_statuses(self):
        return RestfulModelCollection(JobStatus, self)

    @property
    def scheduler(self):
        return SchedulerRestfulModelCollection(self)

    @property
    def components(self):
        return RestfulModelCollection(Component, self)

    @property
    def deltas(self):
        return DeltaCollection(self)

    @property
    def webhooks(self):

Answer:         return RestfulModelCollection(Webhook, self)