diff --git a/.arcconfig b/.arcconfig
deleted file mode 100644
index 0df938ee..00000000
--- a/.arcconfig
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "conduit_uri" : "https://phab.nylas.com/"
-}
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index d65a0389..9d4a19e8 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,6 +1,6 @@
[bumpversion]
commit = True
tag = True
-current_version = 5.2.0
+current_version = 6.15.0
[bumpversion:file:nylas/_client_sdk_version.py]
diff --git a/.coveragerc b/.coveragerc
index 8460a941..cf7ed82e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,2 +1,5 @@
[run]
source = nylas
+omit =
+ tests/*
+ examples/*
diff --git a/.github/workflows/clubhouse.yml b/.github/workflows/clubhouse.yml
deleted file mode 100644
index 9ca4e01f..00000000
--- a/.github/workflows/clubhouse.yml
+++ /dev/null
@@ -1,61 +0,0 @@
-name: Linked Clubhouse Story
-on:
- pull_request:
- types: [opened, closed, labeled]
-
-jobs:
- clubhouse:
- runs-on: ubuntu-latest
- if: ${{ github.event.pull_request.draft == false }}
- steps:
- - uses: singingwolfboy/create-linked-clubhouse-story@v1.7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- clubhouse-token: ${{ secrets.CLUBHOUSE_TOKEN }}
- project-name: Python SDK
- opened-state-name: Reviewing
- merged-state-name: Done
- closed-state-name: Won't Pursue
- ignored-users: dependabot
- label-iteration-group-map: |
- {
- "SDK": {
- "groupId": "6093fae3-f1eb-424f-a84f-cf8ff98167db",
- "excludeName": "Backlog"
- }
- }
- user-map: |
- {
- "AaronDDM": "5f622398-b918-4e0a-8f87-495cbfb63682",
- "BahramKouhestani: "5fa31422-7ab3-46c6-89ef-fbde9120a640",
- "bengotow": "5d659cde-7f97-4da0-88c5-50c5c0c0053e",
- "BenLloydPearson": "5d260325-a60e-4a03-99ee-3b614b1cedfd",
- "benjaminwhtan": "5d89014d-8fda-49a1-8f8c-ce8c72cd1b96",
- "billwjo": "5d2372c8-2f19-4409-a86c-97462c626d10",
- "chorrell": "5f0dd62e-bd28-4eac-aec6-526c7a96b19c",
- "danielliu": "607ddce2-652a-48a5-ae5f-0d42811ede81",
- "davidting": "5e0e4200-3c79-4755-aab9-a4a16f8ddcf1",
- "dominicj-nylas": "5e947e4e-d40e-477e-85d0-21f2b323c069",
- "dtom90": "604fa1a5-f5ce-4c4e-908a-3a87c15db98d",
- "aiirwiick": "60ab71f2-b85d-4317-9018-df3e48e89dab",
- "sammywen": "607da402-9bee-4ca4-80d5-b8f833840732",
- "jesmarcannao": "5e441a1c-a7b5-40c8-8634-ef9f0bcce5c1",
- "jhatch28": "5d0a9c4e-48a5-419b-a9b4-68fa672b96c9",
- "jieunsharonkim": "5d03a446-3f70-4747-8bfd-af099463b4a4",
- "jonafato": "5e9f1493-a9b5-436f-9c8b-c933631bbf8a",
- "jseller": "604625a6-1d18-428e-a825-09233513387a",
- "khamidou": "5d0ab326-3910-4ae8-b2f2-37bf2465d296",
- "kdoby": "5d769b41-2f29-42a0-ac47-73ad93682db4",
- "lkaileh": "5d23983d-49b8-4481-a947-87ff2bc56066",
- "Gerdie": "5e1d52c6-7e82-4015-ab26-a59c357cfd4a",
- "maxwell-schwartz": "5d48497f-0016-46f1-9a3c-149806c3599a",
- "mypile": "60465c2b-1a8b-42dc-a467-d6d084d8f4f8",
- "pfista": "5d0d0771-c540-4206-94b1-47e9fbef88b0",
- "pengfeiye": "5d1533b0-fa0e-4557-a6fc-80e97923d817",
- "peterdemarzio": "5e138a88-26cf-4814-b9fe-0c1f0ddcb8c0",
- "philrenaud": "5e9f7a55-876b-453f-b137-b8677727fb81",
- "spang": "5d13f026-00a6-4a9c-ba80-0048f43427f4",
- "yusra-ahmed": "5d237850-e39b-4e75-8c52-89003602a59a",
- "nylas-marcus": "5fc55708-44b9-41e8-aec0-004c3af90e69",
- "mrashed-dev": "609964df-eb6c-4d74-8799-e33b7d503a09"
- }
diff --git a/.github/workflows/sdk-reference.yml b/.github/workflows/sdk-reference.yml
new file mode 100644
index 00000000..bf16e9ff
--- /dev/null
+++ b/.github/workflows/sdk-reference.yml
@@ -0,0 +1,43 @@
+name: sdk-reference
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ docs:
+ runs-on: ubuntu-latest
+ environment:
+ name: sdk-reference
+ url: ${{ steps.deploy.outputs.url }}
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup Nodejs
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install dependencies and build
+ run: pip install .[docs]
+ - name: Build docs
+ run: python setup.py build-docs
+ - name: Set env BRANCH
+ run: echo "BRANCH=$(echo $GITHUB_REF | cut -d'/' -f 3)" >> $GITHUB_ENV
+ - name: Set env CLOUDFLARE_BRANCH
+ run: |
+ if [[ $BRANCH == 'main' && $GITHUB_EVENT_NAME == 'push' ]]; then
+ echo "CLOUDFLARE_BRANCH=main" >> "$GITHUB_ENV"
+ else
+ echo "CLOUDFLARE_BRANCH=$BRANCH" >> "$GITHUB_ENV"
+ fi
+ - name: Publish to Cloudflare Pages
+ uses: cloudflare/pages-action@v1
+ id: deploy
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ projectName: nylas-python-sdk-reference
+ directory: site
+ wranglerVersion: "3"
+ branch: ${{ env.CLOUDFLARE_BRANCH }}
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 621a14c5..f7763d52 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["2.x", "3.x"]
+ python-version: ["3.8", "3.x"]
name: Python ${{ matrix.python-version }}
steps:
- uses: actions/checkout@v2
@@ -31,20 +31,26 @@ jobs:
- name: Upload coverage to Codecov
if: ${{ always() }}
- uses: codecov/codecov-action@v1.0.7
+ uses: codecov/codecov-action@v3
black:
runs-on: ubuntu-latest
- name: Black
+ name: Pylint and Black
steps:
- uses: actions/checkout@v2
- name: Setup python
uses: actions/setup-python@v2
with:
- python-version: "3.x"
+ python-version: "3.8"
- - name: Install black
- run: pip install black
+ - name: Install dependencies
+ run: pip install .
+
+ - name: Install pylint and black
+ run: pip install pylint black
+
+ - name: Run pylint
+ run: pylint nylas
- name: Run black
- run: black --check .
+ run: black .
diff --git a/.gitignore b/.gitignore
index 66731b8d..2940379b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1,81 @@
-# Byte-compiled / optimized / DLL files
+# UV virtual environment
+.venv/
+.venv
+
+# UV cache and lock files
+.uv/
+uv.lock
+
+# Python cache and compiled files
__pycache__/
*.py[cod]
-
-# C extensions
+*$py.class
*.so
# Distribution / packaging
.Python
-env/
-bin/
build/
develop-eggs/
dist/
+downloads/
eggs/
+.eggs/
+lib/
+lib64/
parts/
sdist/
var/
+wheels/
+share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
+MANIFEST
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
+# PyInstaller
+*.manifest
+*.spec
# Unit test / coverage reports
htmlcov/
.tox/
+.nox/
.coverage
+.coverage.*
.cache
nosetests.xml
coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Environments
+.env
+.venv
+ENV/
+env/
+venv/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# macOS
+.DS_Store
+
+# Docs
+docs/_build/
+site/
+
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
# Translations
*.mo
@@ -62,3 +107,10 @@ local/
pip-selfcheck.json
tests/output
+local.env
+test.py
+.env
+
+# Documentation
+site
+docs
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..f789fc61
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,34 @@
+[FORMAT]
+good-names=id,k,v,cc,to,ip
+max-line-length=120
+
+[MESSAGES CONTROL]
+disable=
+ missing-module-docstring,
+ arguments-differ,
+ protected-access,
+ duplicate-code,
+ too-many-instance-attributes,
+ unnecessary-pass,
+ too-many-arguments,
+ too-few-public-methods,
+
+[TYPECHECK]
+
+generated-members=
+ Message.from_dict,
+ Draft.from_dict,
+ Time.from_dict,
+ Timespan.from_dict,
+ Date.from_dict,
+ Datespan.from_dict,
+ Details.from_dict,
+ Autocreate.from_dict,
+ RequestIdOnlyResponse.from_dict,
+ TokenInfoResponse.from_dict,
+ CodeExchangeResponse.from_dict,
+ NylasApiErrorResponse.from_dict,
+ NylasOAuthErrorResponse.from_dict,
+ FreeBusyError.from_dict,
+ FreeBusy.from_dict,
+ ScheduledMessage.from_dict,
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 54483845..c5e0eed8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,288 @@
nylas-python Changelog
======================
+Unreleased
+----------
+
+v6.15.0
+----------
+* Added Lists support (`Client.lists`, `/v3/lists`): list, create, find, update, and delete lists, plus `list_items`, `add_items`, and `remove_items` for `/v3/lists/{list_id}/items`, with typed request/response models in `nylas.models.lists`
+* Added Manage Domains (`Client.domains`, `/v3/admin/domains`): list, create, find, update, delete, `get_info`, and `verify` with models in `nylas.models.domains`; optional `ServiceAccountSigner` (`nylas.handler.service_account`) for service-account headers (`X-Nylas-Kid`, `X-Nylas-Nonce`, `X-Nylas-Timestamp`, `X-Nylas-Signature`) on each `Domains` method; new `cryptography` dependency, RSA signing, and `HttpClient` `serialized_json_body` so signed payloads match the wire body
+* Added Transactional Send: `Client.transactional_send.send()` for `POST /v3/domains/{domain_name}/messages/send`, with `TransactionalSendMessageRequest` and `TransactionalTemplate` models (JSON and multipart send behavior aligned with grant `messages.send`)
+* Added Policies support (`Client.policies`, `/v3/policies`): list, create, find, update, and delete, with typed request/response models in `nylas.models.policies`
+* Added Rules support (`Client.rules`): list, create, find, update, and delete for `/v3/rules`, plus `list_evaluations` for `/v3/grants/{grant_id}/rule-evaluations`, with typed request/response models in `nylas.models.rules`
+
+v6.14.3
+----------
+* UAS multi-credential update
+* Added `specific_time_availability` field to `AvailabilityParticipant` for overriding open hours on specific dates
+* Added `smtp_required` option to hosted authentication config to require users to enter SMTP settings during IMAP authentication
+* Clarified `ListEventQueryParams` documentation (attribute order, field descriptions, and `expand_recurring` wording)
+
+v6.14.2
+----------
+* Fix UTF-8 encoding for special characters (emoji, accented letters, etc.) by encoding JSON as UTF-8 bytes
+
+v6.14.1
+----------
+* Fix attachment id to not be a requirement
+
+v6.14.0
+----------
+* Added `message.deleted` to the Webhook enum, appended tests
+* Fixed Participant.email not being optional, Microsoft events can now be represented
+* Clarified UTF-8 encoding behavior: ASCII characters are preserved as-is (not escaped) while non-ASCII characters are preserved as UTF-8 in JSON payloads
+* Added support for metadata_pair query params to the messages and drafts list endpoints
+
+v6.13.1
+----------
+* Fixed UTF-8 character encoding for all API requests to preserve special characters (accented letters, emoji, etc.) instead of escaping them as unicode sequences
+
+v6.13.0
+----------
+* Fixed from field handling in messages.send() to properly map "from_" field to "from field
+* Fixed content_id handling for large inline attachments to use content_id as field name instead of generic file{index}
+
+v6.12.0
+----------
+* Added Yahoo, Zoom, EWS as providers to models/auth.py
+* Fixed grants.update() not using the correct "PATCH" method
+* Added support for `is_plaintext` property in messages send and drafts create endpoints
+
+v6.11.1
+----------
+* Fixed KeyError when processing events with empty or incomplete conferencing objects
+
+v6.11.0
+----------------
+* Added `unknown` to ConferencingProvider
+
+v6.10.0
+----------------
+* Added support for `single_level` query parameter in `ListFolderQueryParams` for Microsoft accounts to control folder hierarchy traversal
+* Added support for `earliest_message_date` query parameter for threads
+* Fixed `earliest_message_date` not being an optional response field
+* Added support for new message fields query parameter values: `include_tracking_options` and `raw_mime`
+* Added `tracking_options` field to Message model for message tracking settings
+* Added `raw_mime` field to Message model for Base64url-encoded message data
+* Added TrackingOptions model for message tracking configuration
+* Maintained backwards compatibility for existing message functionality
+* Added support for `include_hidden_folders` query parameter for listing folders (Microsoft only)
+
+v6.9.0
+----------------
+* Added support for for tentative_as_busy parameter to the availability request
+* Added missing webhook triggers
+* Added support for Notetaker APIs
+* Added support for Notetaker via the calendar and event APIs
+
+v6.8.0
+----------------
+* Added support for `list_import_events`
+
+v6.7.0
+----------------
+* Added support for `select` query parameter in list calendars, list events, and list messages.
+
+v6.6.0
+----------------
+* Added response headers to all responses from the Nylas API
+
+v6.5.0
+----------------
+* Added support for Scheduler APIs
+* Added metadata field support for drafts and messages through CreateDraftRequest and Message model
+* Fixed attachment download response handling
+
+v6.4.0
+----------------
+* Added support for from field for sending messages
+* Added missing schedule-specific fields to Message model
+* Added migration grant properties
+* Fixed from field not being optional causing deserialization errors
+* Fixed IMAP identifiers not encoding correctly
+* Fixed NylasOAuthError not setting the status code properly
+
+v6.3.1
+----------------
+* Fixed typo on Clean Messages
+* Fixed request session being reused across multiple requests
+* Added Folder Webhooks
+* Removed use of TestCommand
+
+v6.3.0
+----------------
+* Added Folder query param support
+* Added `master_event_id` field to events
+* Fixed issue with application models not being deserialized correctly
+
+v6.2.0
+----------------
+* Added support for custom headers field for drafts and messages
+* Added support for overriding various fields of outgoing requests
+* Added support for `provider` field in code exchange response
+* Added support for `event_type` filtering field for listing events
+* Added clean messages support
+* Added additional webhook triggers
+* Fixed issue where attachments < 3mb were not being encoded correctly
+* Fixed issue deserializing event and code exchange responses
+
+v6.1.1
+----------------
+* Improved message sending and draft create/update performance
+* Change default timeout to match API (90 seconds)
+
+v6.1.0
+----------------
+* Added support for `round_to` field in availability response
+* Added support for `attributes` field in folder model
+* Added support for icloud as an auth provider
+* Fixed webhook secret not returning on creation of webhook
+* Fixed issue with free busy and scheduled message responses not being deserialized correctly
+* Removed `client_id` from `detect_provider()`
+
+v6.0.1
+----------------
+* Fix deserialization error when getting token info or verifying access token
+* Fix schemas issue in the `Event` and `CodeExchangeResponse` models
+
+v6.0.0
+----------------
+* **BREAKING CHANGE**: Python SDK v6 supports the Nylas API v3 exclusively, dropping support for any endpoints that are not available in v3
+* **BREAKING CHANGE**: Drop support for Python < v3.8
+* **BREAKING CHANGE**: Dropped the use of 'Collections' in favor of 'Resources'
+* **BREAKING CHANGE**: Removed all REST calls from models and moved them directly into resources
+* **BREAKING CHANGE**: Models no longer inherit from `dict` but instead either are a `dataclass` or inherit from `TypedDict`
+* **BREAKING CHANGE**: Renamed the SDK entrypoint from `APIClient` to `Client`
+* **REMOVED**: Local Webhook development support is removed due to incompatibility
+* Rewritten the majority of SDK to be more intuitive, explicit, and efficient
+* Created models for all API resources and endpoints, for all HTTP methods to reduce confusion on which fields are available for each endpoint
+* Created error classes for the different API errors as well as SDK-specific errors
+
+v5.14.1
+----------------
+* Fix error when trying to iterate on list after calling count
+* Fix error when setting participant status on create event
+
+v5.14.0
+----------------
+* Add support for `view` parameter in `Threads.search()`
+
+v5.14.1
+----------------
+* Fix error when trying to iterate on list after calling count
+* Fix error when setting participant status on create event
+
+v5.14.0
+----------------
+* Add support for verifying webhook signatures
+* Add optional parameter for token-info endpoint
+
+v5.13.1
+----------------
+* Fix `send_authorization` not returning the correct dict
+* Fix expanded threads not inflating the messages objects properly
+* Fix class attributes with leading underscores not serializing as expected
+
+v5.13.0
+----------------
+* Add local webhook development support
+* Use PEP508 syntax for conditional dependencies
+
+v5.12.1
+----------------
+* Only install enum34 on python34 and below
+
+v5.12.0
+----------------
+* Add support for sending raw MIME messages
+
+v5.11.0
+----------------
+* Add support for calendar colors (for Microsoft calendars)
+* Add support for rate limit errors
+* Add support for visibility field in Event
+
+v5.10.2
+----------------
+* Update package setup to be compatible with PEP 517
+
+v5.10.1
+----------------
+* Fix authentication for integrations
+
+v5.10.0
+----------------
+* Add `metadata` field to `JobStatus`
+* Add missing hosted authentication parameters
+* Add support for `calendar` field in free-busy, availability, and consecutive availability queries
+
+v5.9.2
+----------------
+* Add `enforce_read_only` parameter to overriding `as_json` functions
+
+v5.9.1
+----------------
+* Add option to include read only params in `as_json`
+* Change config file in `hosted-oauth` example to match new Flask rules
+* Fix unauthorized error for `revoke_token`
+
+v5.9.0
+----------------
+* Add support for collective and group events
+
+v5.8.0
+----------------
+* Add support for getting the number of queried objects (count view)
+* Improve usage of read only fields in models
+* Fix Calendar availability functions not using the correct authentication method
+
+v5.7.0
+----------------
+* Add Outbox support
+* Add support for new (beta) Integrations authentication (Integrations API, Grants API, Hosted Authentication for Integrations)
+* Add support for `limit` and `offset` for message/thread search
+* Add `authentication_type` field to `Account`
+* Enable Nylas API v2.5 support
+* Fix `Draft` not sending metadata
+
+v5.6.0
+----------------
+* Add Delta support
+* Add Webhook support
+* Omit `None` values from resulting `as_json()` object
+* Enable Nylas API v2.4 support
+
+v5.5.1
+----------------
+* Add validation for `send_authorization`
+* Fix `native-authentication-gmail` example app
+
+v5.5.0
+----------------
+* Add support for `Event` to ICS
+* Enable full payload response for exchanging the token for code
+
+v5.4.2
+----------------
+* Add missing `source` field in `Contact` class
+
+v5.4.1
+----------------
+* Fix issue where keyword arguments calling `_update_resource` were not correctly resolving to URL params
+* Improved support for Application Details
+
+v5.4.0
+----------------
+* Add job status support
+* Add `is_primary` field to Calendar
+* Fix bug where updating an Event results in an API error
-Unreleased (dev)
+v5.3.0
----------------
+* Add support for Scheduler API
* Add support for Event notifications
* Add support for Component CRUD
+* Add metadata support for `Calendar`, `Message` and `Account`
* Improve error details returned from the API
v5.2.0
@@ -23,7 +301,7 @@ v5.0.0
* Transitioned from `app_id` and `app_secret` naming to `client_id` and `client_secret`
* Add support for the Nylas Neural API
* Add `metadata` field in the Event model to support new event metadata feature
-* Add new Room Resource fields
+* Add new Room Resource fields
* Add `Nylas-API-Version` header support
* Fix adding a tracking object to an existing `draft`
* Fix issue when converting offset-aware `datetime` objects to `timestamp`
diff --git a/Contributing.md b/Contributing.md
index 3954c295..76a78806 100644
--- a/Contributing.md
+++ b/Contributing.md
@@ -3,6 +3,160 @@
The following is a set of guidelines for contributing to the Nylas Python SDK; these are guidelines, not rules, so please use your best judgement and feel free to propose changes to this document via pull request.
+# Development Setup
+
+To get started contributing to this repository, you'll need to set up a local development environment. Follow these steps:
+
+## Prerequisites
+
+- Python 3.8 or higher (the project supports Python 3.8+)
+- Git
+- A GitHub account
+
+## Setup Steps
+
+### 1. Fork and Clone the Repository
+
+```bash
+# Fork the repository on GitHub, then clone your fork
+git clone https://github.com/YOUR_USERNAME/nylas-python.git
+cd nylas-python
+
+# Add the upstream repository as a remote
+git remote add upstream https://github.com/nylas/nylas-python.git
+```
+
+### 2. Set Up Python Virtual Environment
+
+We recommend using a virtual environment to isolate your development dependencies:
+
+```bash
+# Create a virtual environment
+python3 -m venv .venv
+
+# Activate the virtual environment
+# On macOS/Linux:
+source .venv/bin/activate
+
+# On Windows:
+# .venv\Scripts\activate
+```
+
+**Important**: If you encounter issues with `pip` not being available in the virtual environment, run:
+
+```bash
+# Ensure pip is available in the virtual environment
+python -m ensurepip --upgrade
+```
+
+### 3. Install Development Dependencies
+
+Install the package in editable mode with all optional dependencies:
+
+```bash
+# Install the package in development mode with all optional dependencies
+python -m pip install -e ".[test,docs,release]"
+
+# Or install specific dependency groups as needed:
+# python -m pip install -e ".[test]" # For running tests
+# python -m pip install -e ".[docs]" # For building documentation
+# python -m pip install -e ".[release]" # For release management
+```
+
+**Note**: We use `python -m pip` instead of just `pip` to ensure we're using the pip from the virtual environment.
+
+### 4. Install Code Quality Tools
+
+Install the linting and formatting tools used by the project:
+
+```bash
+python -m pip install pylint black
+```
+
+### 5. Verify Your Setup
+
+Run the tests to make sure everything is working correctly:
+
+```bash
+# Run the test suite
+python setup.py test
+
+# Or run tests with pytest directly
+pytest
+
+# Run with coverage
+pytest --cov=nylas tests/
+```
+
+Check code formatting and linting:
+
+```bash
+# Check code formatting (this will modify files)
+black .
+
+# Run linting
+pylint nylas
+```
+
+## Development Workflow
+
+1. **Create a branch** for your feature or bug fix:
+ ```bash
+ git checkout -b your-feature-branch
+ ```
+
+2. **Make your changes** and write tests for any new functionality
+
+3. **Run tests and linting**:
+ ```bash
+ python setup.py test
+ black .
+ pylint nylas
+ ```
+
+4. **Commit your changes** following [conventional commit practices](https://www.conventionalcommits.org/)
+
+5. **Push to your fork** and create a pull request
+
+## Project Structure
+
+- `nylas/` - Main SDK source code
+- `tests/` - Test files
+- `examples/` - Example usage scripts
+- `scripts/` - Build and development scripts
+- `pyproject.toml` - Project configuration and dependencies
+- `setup.py` - Legacy setup file (still used for some operations)
+
+## Running Tests
+
+The project uses pytest for testing:
+
+```bash
+# Run all tests
+pytest
+
+# Run tests with coverage
+pytest --cov=nylas tests/
+
+# Run specific test files
+pytest tests/test_specific_module.py
+
+# Run tests matching a pattern
+pytest -k "test_pattern"
+```
+
+## Documentation
+
+To build the documentation locally:
+
+```bash
+# Make sure you have docs dependencies installed
+pip install -e ".[docs]"
+
+# Generate documentation (if there are scripts for this)
+python scripts/generate-docs.py
+```
+
# How to Ask a Question
If you have a question about how to use the Python SDK, please [create an issue](https://github.com/nylas/nylas-python/issues) and label it as a question. If you have more general questions about the Nylas Communications Platform, or the Nylas Email, Calendar, and Contacts API, please reach out to support@nylas.com to get help.
diff --git a/README.md b/README.md
index e2f9bad7..f923483d 100644
--- a/README.md
+++ b/README.md
@@ -1,58 +1,190 @@
-# Nylas Python SDK  [](https://codecov.io/gh/nylas/nylas-python)
+
+
+
+
-This is the GitHub repository for the Nylas Python SDK and this repo is primarily for anyone who wants to make contributions to the SDK or install it from source. If you are looking to use Python to access the Nylas Email, Calendar, or Contacts API you should refer to our official [Python SDK Quickstart Guide](https://docs.nylas.com/docs/quickstart-python).
+
Nylas Python SDK
-The Nylas Communications Platform provides REST APIs for [Email](https://docs.nylas.com/docs/quickstart-email), [Calendar](https://docs.nylas.com/docs/quickstart-calendar), and [Contacts](https://docs.nylas.com/docs/quickstart-contacts), and the Python SDK is the quickest way to build your integration using Python
+
+ The official Python SDK for Nylas โ the infrastructure that powers communications
+
-Here are some resources to help you get started:
+
+
+
+
+
+
-- [Nylas SDK Tutorials](https://docs.nylas.com/docs/tutorials)
-- [Get Started with the Nylas Communications Platform](https://docs.nylas.com/docs/getting-started)
-- [Sign up for your Nylas developer account.](https://nylas.com/register)
-- [Nylas API Reference](https://docs.nylas.com/reference)
+
-If you have a question about the Nylas Communications Platform, please reach out to support@nylas.com to get help.
+
-# Install
+The official Python SDK for [Nylas](https://developer.nylas.com/docs/v3/) โ the infrastructure that powers communications. Integrate with Gmail, Microsoft, IMAP, Zoom, and 250+ email, calendar, and meeting providers in 5 minutes. Covers [Email](https://developer.nylas.com/docs/v3/email/), [Calendar](https://developer.nylas.com/docs/v3/calendar/), [Contacts](https://developer.nylas.com/docs/v3/email/contacts/), [Scheduler](https://developer.nylas.com/docs/v3/scheduler/), [Notetaker](https://developer.nylas.com/docs/v3/notetaker/), and [Agent Accounts](https://developer.nylas.com/docs/v3/agent-accounts/).
-The Nylas Python SDK is available via pip:
+This repository is for contributors and anyone installing the SDK from source. If you just want to use the SDK in your app, head straight to the [**Python SDK guide**](https://developer.nylas.com/docs/v3/sdks/python/) on developer.nylas.com.
-`pip install nylas`
+## Get started
-To install the SDK from source, clone this repo and run the install script.
+1. [Sign up for a free Nylas account](https://dashboard-v3.nylas.com/register) and grab your API key from the [Nylas Dashboard](https://dashboard-v3.nylas.com/).
+2. Read the [Getting started guide](https://developer.nylas.com/docs/v3/getting-started/) for the core concepts (applications, grants, API keys).
+3. Install the SDK and make your first request โ see below.
- git clone https://github.com/nylas/nylas-python.git && cd nylas-python
- python setup.py install
+You can also bootstrap from the terminal:
-# Usage
+```bash
+brew install nylas/nylas-cli/nylas
+nylas init
+```
-To use this SDK, you first need to [sign up for a free Nylas developer account](https://nylas.com/register).
+More options in the [CLI getting-started guide](https://cli.nylas.com/guides/getting-started).
-Then, follow our guide to [setup your first app and get your API access keys](https://docs.nylas.com/docs/get-your-developer-api-keys).
+## โ๏ธ Install
-Next, in your python script, import the `APIClient` class from the `nylas` package, and create a new instance of this class, passing the variables you gathered when you got your developer API keys. In the following example, replace `CLIENT_ID`, `CLIENT_SECRET`, and `ACCESS_TOKEN` with your values.
+> **Requirements:** Python 3.8 or later.
+```bash
+pip install nylas
+```
- from nylas import APIClient
+To install from source:
- nylas = APIClient(
- CLIENT_ID,
- CLIENT_SECRET,
- ACCESS_TOKEN
- )
+```bash
+git clone https://github.com/nylas/nylas-python.git
+cd nylas-python
+pip install -e .
+```
-Now, you can use `nylas` to access full email, calendar, and contacts functionality. For example, here is how you would print the subject line for the most recent email message to the console.
+### Runtime support
+Tested on CPython 3.8+. Runs on standard servers as well as serverless platforms like AWS Lambda, Google Cloud Functions, and Vercel โ install `nylas` like any other dependency in your deployment package.
- message = nylas.messages.first()
- print(message.subject)
+## โก๏ธ Usage
-To learn more about how to use the Nylas Python SDK, please refer to our [Python SDK QuickStart Guide](https://docs.nylas.com/docs/quickstart-python) and our [Python tutorials](https://docs.nylas.com/docs/tutorials).
+You access Nylas resources (messages, calendars, events, contacts, โฆ) through an instance of `Client`. Initialize it with your API key โ and optionally an `api_uri` matching your [data residency](https://developer.nylas.com/docs/dev-guide/platform/data-residency/).
-# Contributing
+```python
+import os
+from nylas import Client
-Please refer to [Contributing](Contributing.md) for information about how to make contributions to this project. We welcome questions, bug reports, and pull requests.
+nylas = Client(
+ api_key=os.environ["NYLAS_API_KEY"],
+ api_uri=os.environ.get("NYLAS_API_URI", "https://api.us.nylas.com"),
+ timeout=30, # optional, in seconds
+)
+```
-# License
+Once initialized, use it to make requests against a [grant](https://developer.nylas.com/docs/v3/auth/) (an authenticated end-user account):
-This project is licensed under the terms of the MIT license. Please refer to [LICENSE](LICENSE) for the full terms.
+```python
+calendars, request_id, next_cursor = nylas.calendars.list(
+ identifier=os.environ["NYLAS_GRANT_ID"],
+)
+print(calendars)
+```
+
+Resources expose a consistent CRUD surface โ `create()`, `find()`, `list()`, `update()`, `destroy()` โ plus resource-specific methods (e.g. `messages.send()`, `events.send_rsvp()`). Request and response models are [`dataclasses-json`](https://github.com/lidatong/dataclasses-json) dataclasses, so every payload is fully type-hinted and supports `to_dict()` / `from_dict()`.
+
+### Error handling
+
+The SDK raises typed exceptions you can catch and inspect. Every API error carries a `request_id` and `status_code` โ include both when filing a support ticket so we can trace the request end-to-end.
+
+```python
+from nylas import Client
+from nylas.models.errors import (
+ NylasApiError,
+ NylasOAuthError,
+ NylasSdkTimeoutError,
+)
+
+try:
+ nylas.calendars.list(identifier=grant_id)
+except NylasApiError as err:
+ print(err.status_code, err.type, str(err), err.request_id)
+except NylasOAuthError as err:
+ print(err.error, err.error_code, err.error_description)
+except NylasSdkTimeoutError as err:
+ print("Timed out:", err.url, err.timeout)
+```
+
+Step-by-step walkthroughs in the SDK guide:
+
+- [Send messages](https://developer.nylas.com/docs/v3/sdks/python/send-email/)
+- [Read messages and threads](https://developer.nylas.com/docs/v3/sdks/python/read-messages-threads/)
+- [Manage events on a calendar](https://developer.nylas.com/docs/v3/sdks/python/manage-events/)
+- [Manage contacts](https://developer.nylas.com/docs/v3/sdks/python/manage-contacts/)
+- [Manage folders and labels](https://developer.nylas.com/docs/v3/sdks/python/manage-folders-labels/)
+
+### Debugging
+
+To inspect the raw HTTP traffic the SDK sends, turn on `requests`-level logging:
+
+```python
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+logging.getLogger("urllib3").setLevel(logging.DEBUG)
+```
+
+## ๐ก Examples
+
+Runnable examples live in [`examples/`](examples/) โ including [send email](examples/send_email_demo/), [inline attachments](examples/inline_attachment_demo/), [folders](examples/folders_demo/), [import events](examples/import_events_demo/), [Notetaker API](examples/notetaker_api_demo/), [Notetaker calendar](examples/notetaker_calendar_demo/), [message fields](examples/message_fields_demo/), [metadata fields](examples/metadata_field_demo/), [provider errors](examples/provider_error_demo/), [response headers](examples/response_headers_demo/), [`select` parameter](examples/select_param_demo/), [special characters](examples/special_characters_demo/), [hidden folders](examples/include_hidden_folders_demo/), and [plain text](examples/is_plaintext_demo/).
+
+For full sample apps and product quickstarts, browse [**nylas-samples** on GitHub](https://github.com/orgs/nylas-samples/repositories?q=python) โ every official SDK has Email, Calendar, Contacts, Scheduler, and Webhooks quickstarts.
+
+## ๐ค AI agents
+
+[nylas/skills](https://github.com/nylas/skills) drops Nylas into Claude Code, Cursor, Codex, and other agents that support the skills format:
+
+```bash
+npx skills add nylas/skills
+/plugin marketplace add nylas/skills # Claude Code
+```
+
+The CLI also installs an MCP server for Claude Desktop, Claude Code, Cursor, Windsurf, or VS Code:
+
+```bash
+brew install nylas/nylas-cli/nylas
+nylas mcp install
+```
+
+Walkthrough: [give AI agents email access via MCP](https://cli.nylas.com/guides/give-ai-agents-email-access-via-mcp).
+
+## ๐ Reference
+
+- **SDK guide:** [developer.nylas.com/docs/v3/sdks/python](https://developer.nylas.com/docs/v3/sdks/python/)
+- **API reference:** [developer.nylas.com/docs/api/v3](https://developer.nylas.com/docs/api/v3/)
+- **Python SDK reference:** [nylas-python-sdk-reference.pages.dev](https://nylas-python-sdk-reference.pages.dev/) โ generated method/class docs for this SDK
+- **Webhooks (notifications):** [developer.nylas.com/docs/v3/notifications](https://developer.nylas.com/docs/v3/notifications/)
+- **Auth flows:** [developer.nylas.com/docs/v3/auth](https://developer.nylas.com/docs/v3/auth/)
+- **Dev guide & best practices:** [developer.nylas.com/docs/dev-guide](https://developer.nylas.com/docs/dev-guide/)
+- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
+
+## โจ Upgrading
+
+See [`CHANGELOG.md`](CHANGELOG.md) for per-release notes. Older upgrade guidance (v5.x โ v6.x) lives in [`UPGRADE.md`](UPGRADE.md).
+
+## ๐ Contributing
+
+Issues, ideas, and pull requests welcome โ see [Contributing.md](Contributing.md). Before opening a large change, please open an issue or post in the [forum](https://forums.nylas.com) so we can sanity-check the direction.
+
+## ๐ Security
+
+Found a vulnerability? Please **don't** open a public issue. Report it through our [Vulnerability Disclosure Policy](https://www.nylas.com/security/vulnerability-disclosure-policy/).
+
+## ๐ Other Nylas SDKs
+
+- [nylas-nodejs](https://github.com/nylas/nylas-nodejs) ยท `npm install nylas`
+- [nylas-ruby](https://github.com/nylas/nylas-ruby) ยท `gem install nylas`
+- [nylas-java](https://github.com/nylas/nylas-java) ยท Maven / Gradle (Kotlin too)
+
+## ๐ License
+
+MIT โ see [LICENSE](LICENSE).
diff --git a/UPGRADE.md b/UPGRADE.md
new file mode 100644
index 00000000..4283f36e
--- /dev/null
+++ b/UPGRADE.md
@@ -0,0 +1,178 @@
+# Upgrade to the Nylas Python SDK v6.0
+
+The Nylas Python SDK has been rewritten to prepare for the upcoming release of the Nylas API v3. The changes make the SDK more idiomatic and easier to use. We've also included [function and model documentation](https://nylas-python-sdk-reference.pages.dev/), so you can easily find the implementation details that you need.
+
+This guide will help you upgrade your environment to use the new SDK.
+
+## Initial setup
+
+To upgrade to the new Python SDK, you must update your dependencies to use the new version. You can do this by installing the newest version of the SDK using pip:
+
+```bash
+pip install nylas --pre
+```
+
+**Note**: The minimum Python version is now the lowest supported LTS: Python v3.8.
+
+The first step to using the new SDK is to initialize a new `nylas` instance. You can do this by passing your API key to the constructor:
+
+```python
+from nylas import Client
+
+nylas = Client(
+ api_key="API_KEY",
+)
+```
+
+Note that the SDK's entry point has changed to `Client`.
+
+From here, you can use the Nylas `Client` instance to make API requests by accessing the different resources configured with your API key.
+
+## Models
+
+Models have completely changed in the new version of the Nylas Python SDK. First, the SDK now includes a specific model for each request and response to/from the Nylas API. Let's take a Calendar object, for example. In the previous version of the SDK, there was only one `Calendar` object representing a Calendar in three states:
+
+- It is to be created.
+- It is to be updated.
+- It is to be retrieved.
+
+This meant that all models had to be configured with _all_ possible fields that could be used in any of these scenarios, making the object very large and difficult to anticipate as a developer.
+
+The new SDK has split the `Calendar` model into three separate models, one for each of the previous scenarios:
+
+- `Calendar`: Retrieve a Calendar.
+- `CreateCalendarRequest`: Create a Calendar.
+- `UpdateCalendarRequest`: Update a Calendar.
+
+Because the new version of the SDK drops support for Python versions lower than v3.8, our models now take advantage of some new Python features. For the models that represent response objects, we now use [dataclasses](https://docs.python.org/3/library/dataclasses.html) to make them more readable, easier to use, and to provide some type hinting and in-IDE hinting. Response objects also implement [the `dataclasses-json` library](https://pypi.org/project/dataclasses-json/), which provides utility functions such as `to_dict()` and `to_json()` that allow you to use your data in a variety of formats.
+
+For models that represent request objects, we're using [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict) to provide a seamless guided experience to building objects for outgoing requests. Both sets of classes are fully typed as well, ensuring that you have all the information you need to make a successful API request.
+
+## Make requests to the Nylas API
+
+To make requests to the Nylas API, you use the `nylas` instance that you configured earlier.
+
+The Python SDK is organized into different resources corresponding to each of the Nylas APIs. Each resource includes all of the available methods to make requests to its respective API. For example, you can use this code to get a list of Calendars:
+
+```python
+from nylas import Client
+
+nylas = Client(
+ api_key="API_KEY",
+)
+
+response = nylas.calendars.list(identifier="GRANT_ID")
+```
+
+This may look very similar to how you would get a list of Calendars in previous versions of the SDK, but there are some key differences that we'll cover in the following sections.
+
+### Response objects
+
+The Nylas API v3 has standard response objects for all requests, with the exception of OAuth endpoints. There are generally two main types of response objects:
+
+- `Response`: Used for requests that return a single object, such as requests to retrieve a single Calendar. This returns a parameterized object of the type that you requested (for example, `Calendar`) and a string representing the request ID.
+- `ListResponse`: Used for requests that return a list of objects, such as requests to retrieve a _list_ of Calendars. This returns a list of parameterized objects of the type that you requested (for example, `Calendar`), a string representing the request ID, and a string representing the token of the next page for paginating the request.
+
+Both classes also support destructuring. This means you can use code like this to manipulate the data:
+
+```python
+from nylas import Client
+
+nylas = Client(
+ api_key="API_KEY",
+)
+
+response = nylas.calendars.list(identifier="GRANT_ID")
+calendars = response.data # The list of calendars
+
+# Or
+
+calendars, request_id = nylas.calendars.list(identifier="CALENDAR_ID") # The list of calendars and the request ID
+```
+
+### Pagination
+
+The Nylas API v3 uses a new way to paginate responses by returning a `next_cursor` parameter in `ListResponse` objects. The `next_cursor` points to the next page, if one exists.
+
+Currently, the Nylas Python SDK doesn't support pagination out of the box, but this is something we're looking to add in the future. Instead, you can use `next_cursor` to make a request to the next page:
+
+```python
+from nylas import Client
+
+nylas = Client(
+ api_key="API_KEY",
+)
+
+response = nylas.calendars.list(identifier="GRANT_ID")
+all_calendars = list(response)
+
+while response.next_cursor:
+ response = nylas.calendars.list(identifier="GRANT_ID", query_params={"page_token": response.next_cursor})
+ all_calendars.extend(response)
+```
+
+### Error objects
+
+Similar to response objects, the Nylas API v3 has standard error objects for all requests, with the exception of OAuth endpoints. There are two superclass error classes:
+
+- `AbstractNylasApiError`: Used for errors returned by the Nylas API.
+- `AbstractNylasSdkError`: Used for errors returned by the Python SDK.
+
+The `AbstractNylasApiError` superclass includes two subclasses:
+
+- `NylasOAuthError`: Used for Nylas API errors returned from OAuth endpoints.
+- `NylasApiError`: Used for all other Nylas API errors.
+
+The Python SDK extracts error details from the response and stores them in the error object, along with the request ID and HTTP status code.
+
+Currently, there is only one type of `AbstractNylasSdkError` that we return: the `NylasSdkTimeoutError`, which is thrown when a request times out.
+
+## Authentication
+
+The Nylas Python SDK's authentication methods reflect [those available in the Nylas API v3](https://developer.nylas.com/docs/developer-guide/v3-authentication/).
+
+While you can only create and manage your application's connectors (formerly called "integrations") in the Dashboard, you can manage almost everything else directly from the Python SDK. This includes managing Grants, redirect URIs, OAuth tokens, and authenticating your users.
+
+There are two main methods to focus on when authenticating users to your app:
+
+- `Auth#url_for_oath2`: Returns the URL that you should direct your users to in order to authenticate them with OAuth 2.0.
+- `Auth#exchange_code_for_token`: Exchanges the code Nylas returns from the authentication redirect for an access token from the OAuth provider. Nylas' response to this request returns both the access token and information about the new Grant.
+
+Note that you don't need to use the `grant_id` to make requests. Instead, you can use the authenticated email address associated with the Grant as the identifier. If you prefer to use the `grant_id`, you can extract it from the `CodeExchangeResponse`.
+
+This code demonstrates how to authenticate a user into a Nylas app:
+
+```python
+from nylas import Client
+
+nylas = Client(
+ api_key="API_KEY",
+)
+
+# Build the URL for authentication
+auth_url = nylas.auth.url_for_oauth2({
+ "client_id": "CLIENT_ID",
+ "redirect_uri": "abc",
+ "login_hint": "example@email.com"
+})
+
+# Write code here to redirect the user to the url and parse the code
+...
+
+# Exchange the code for an access token
+
+code_exchange_response = nylas.auth.exchange_code_for_token({
+ "client_id": "CLIENT_ID",
+ "client_secret": "CLIENT_SECRET",
+ "code": "CODE",
+ "redirect_uri": "abc"
+})
+
+# Now you can either use the email address that was authenticated or the grant ID in the response as the identifier
+
+response_with_email = nylas.calendars.list(identifier="example@email.com")
+
+# Or
+
+response_with_grant = nylas.calendars.list(identifier=code_exchange_response.grant_id)
+```
diff --git a/diagrams/nylas-logo.png b/diagrams/nylas-logo.png
new file mode 100644
index 00000000..87ad1f17
Binary files /dev/null and b/diagrams/nylas-logo.png differ
diff --git a/examples/folders_demo/README.md b/examples/folders_demo/README.md
new file mode 100644
index 00000000..413d2a36
--- /dev/null
+++ b/examples/folders_demo/README.md
@@ -0,0 +1,64 @@
+# Folders Single Level Parameter Example
+
+This example demonstrates how to use the `single_level` query parameter when listing folders to control folder hierarchy traversal for Microsoft accounts.
+
+## Overview
+
+The `single_level` parameter is a Microsoft-only feature that allows you to control whether the folders API returns:
+- **`single_level=true`**: Only top-level folders (single-level hierarchy)
+- **`single_level=false`**: All folders including nested ones (multi-level hierarchy, default)
+
+This parameter is useful for:
+- **Performance optimization**: Reducing response size when you only need top-level folders
+- **UI simplification**: Building folder trees incrementally
+- **Microsoft-specific behavior**: Taking advantage of Microsoft's folder hierarchy structure
+
+## Prerequisites
+
+- Nylas API key
+- Nylas grant ID for a Microsoft account (this parameter only works with Microsoft accounts)
+
+## Setup
+
+1. Install the SDK in development mode:
+ ```bash
+ cd /path/to/nylas-python
+ pip install -e .
+ ```
+
+2. Set your environment variables:
+ ```bash
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_microsoft_grant_id"
+ ```
+
+## Running the Example
+
+```bash
+python examples/folders_demo/folders_single_level_example.py
+```
+
+## What the Example Demonstrates
+
+1. **Multi-level folder hierarchy** (default behavior)
+2. **Single-level folder hierarchy** using `single_level=true`
+3. **Combined parameters** showing how to use `single_level` with other query parameters
+4. **Hierarchy comparison** showing the difference in folder counts
+
+## Expected Output
+
+The example will show:
+- Folders returned with multi-level hierarchy
+- Folders returned with single-level hierarchy only
+- Count comparison between the two approaches
+- How to combine the parameter with other options like `limit` and `select`
+
+## Use Cases
+
+- **Folder tree UI**: Load top-level folders first, then expand as needed
+- **Performance**: Reduce API response size for Microsoft accounts with deep folder structures
+- **Microsoft-specific integrations**: Take advantage of Microsoft's native folder organization
+
+## Note
+
+This parameter only works with Microsoft accounts. If you use it with other providers, it will be ignored.
\ No newline at end of file
diff --git a/examples/folders_demo/folders_single_level_example.py b/examples/folders_demo/folders_single_level_example.py
new file mode 100644
index 00000000..335b464f
--- /dev/null
+++ b/examples/folders_demo/folders_single_level_example.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using Single Level Parameter for Folders
+
+This example demonstrates how to use the 'single_level' query parameter when listing folders
+to control the folder hierarchy traversal for Microsoft accounts.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID (must be a Microsoft account)
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_microsoft_grant_id"
+ python examples/folders_demo/folders_single_level_example.py
+"""
+
+import os
+import sys
+import json
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_folders(folders: list, title: str) -> None:
+ """Pretty print the folders with a title."""
+ print(f"\n{title}:")
+ if not folders:
+ print(" No folders found.")
+ return
+
+ for folder in folders:
+ # Convert to dict and pretty print relevant fields
+ folder_dict = folder.to_dict()
+ print(
+ f" - {folder_dict.get('name', 'Unknown')} (ID: {folder_dict.get('id', 'Unknown')})"
+ )
+ if folder_dict.get("parent_id"):
+ print(f" Parent ID: {folder_dict['parent_id']}")
+ if folder_dict.get("child_count") is not None:
+ print(f" Child Count: {folder_dict['child_count']}")
+
+
+def demonstrate_multi_level_folders(client: Client, grant_id: str) -> None:
+ """Demonstrate multi-level folder hierarchy (default behavior)."""
+ print("\n=== Multi-Level Folder Hierarchy (Default) ===")
+
+ # Default behavior - retrieves folders across multi-level hierarchy
+ print("\nFetching folders with multi-level hierarchy (single_level=False):")
+ folders = client.folders.list(
+ identifier=grant_id, query_params={"single_level": False}
+ )
+ print_folders(folders.data, "Multi-level folder hierarchy")
+
+ # Also demonstrate without explicitly setting single_level (default behavior)
+ print("\nFetching folders without single_level parameter (default behavior):")
+ folders = client.folders.list(identifier=grant_id)
+ print_folders(folders.data, "Default folder hierarchy (multi-level)")
+
+
+def demonstrate_single_level_folders(client: Client, grant_id: str) -> None:
+ """Demonstrate single-level folder hierarchy."""
+ print("\n=== Single-Level Folder Hierarchy ===")
+
+ # Single-level hierarchy - only retrieves folders from the top level
+ print("\nFetching folders with single-level hierarchy (single_level=True):")
+ folders = client.folders.list(
+ identifier=grant_id, query_params={"single_level": True}
+ )
+ print_folders(folders.data, "Single-level folder hierarchy")
+
+
+def demonstrate_combined_parameters(client: Client, grant_id: str) -> None:
+ """Demonstrate single_level combined with other parameters."""
+ print("\n=== Combined Parameters ===")
+
+ # Combine single_level with other query parameters
+ print("\nFetching limited single-level folders with select fields:")
+ folders = client.folders.list(
+ identifier=grant_id,
+ query_params={
+ "single_level": True,
+ "limit": 5,
+ "select": "id,name,parent_id,child_count",
+ },
+ )
+ print_folders(folders.data, "Limited single-level folders with selected fields")
+
+
+def compare_hierarchies(client: Client, grant_id: str) -> None:
+ """Compare single-level vs multi-level folder counts."""
+ print("\n=== Hierarchy Comparison ===")
+
+ # Get multi-level count
+ multi_level_folders = client.folders.list(
+ identifier=grant_id, query_params={"single_level": False}
+ )
+ multi_level_count = len(multi_level_folders.data)
+
+ # Get single-level count
+ single_level_folders = client.folders.list(
+ identifier=grant_id, query_params={"single_level": True}
+ )
+ single_level_count = len(single_level_folders.data)
+
+ print(f"\nFolder count comparison:")
+ print(f" Multi-level hierarchy: {multi_level_count} folders")
+ print(f" Single-level hierarchy: {single_level_count} folders")
+
+ if multi_level_count > single_level_count:
+ print(
+ f" Difference: {multi_level_count - single_level_count} folders in sub-hierarchies"
+ )
+ elif single_level_count == multi_level_count:
+ print(" No nested folders detected in this account")
+
+
+def main():
+ """Main function demonstrating single_level parameter usage for folders."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(api_key=api_key)
+
+ print("\nDemonstrating Single Level Parameter for Folders")
+ print("===============================================")
+ print("This parameter is Microsoft-only and controls folder hierarchy traversal")
+ print(f"Using Grant ID: {grant_id}")
+
+ try:
+ # Demonstrate different folder hierarchy options
+ demonstrate_multi_level_folders(client, grant_id)
+ demonstrate_single_level_folders(client, grant_id)
+ demonstrate_combined_parameters(client, grant_id)
+ compare_hierarchies(client, grant_id)
+
+ print("\n=== Summary ===")
+ print("โข single_level=True: Returns only top-level folders (Microsoft only)")
+ print("โข single_level=False: Returns folders from all levels (default)")
+ print("โข This parameter helps optimize performance for Microsoft accounts")
+ print("โข Can be combined with other query parameters like limit and select")
+
+ except Exception as e:
+ print(f"\nError: {e}")
+ print("\nNote: This example requires a Microsoft grant ID.")
+ print("The single_level parameter only works with Microsoft accounts.")
+
+ print("\nExample completed!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/hosted-oauth/README.md b/examples/hosted-oauth/README.md
deleted file mode 100644
index 6bc04d12..00000000
--- a/examples/hosted-oauth/README.md
+++ /dev/null
@@ -1,86 +0,0 @@
-# Example: Hosted OAuth
-
-This is an example project that demonstrates how to connect to Nylas via
-OAuth, using [Nylas' hosted OAuth flow](https://docs.nylas.com/reference#oauth).
-
-This example uses the [Flask](http://flask.pocoo.org/) web framework to make
-a small website, and uses the [Flask-Dance](http://flask-dance.rtfd.org/)
-extension to handle the tricky bits of implementing the
-[OAuth protocol](https://oauth.net/).
-Once the OAuth communication is in place, this example website will contact
-the Nylas API to learn some basic information about the current user,
-such as the user's name and email address. It will display that information
-on the page, just to prove that it can fetch it correctly.
-
-In order to successfully run this example, you need to do the following things:
-
-## Get a client ID & client secret from Nylas
-
-To do this, make a [Nylas Developer](https://developer.nylas.com/) account.
-You should see your client ID and client secret on the dashboard,
-once you've logged in on the
-[Nylas Developer](https://developer.nylas.com/) website.
-
-## Update the `config.json` File
-
-Open the `config.json` file in this directory, and replace the example
-client ID and client secret with the real values that you got from the Nylas
-Developer dashboard. You'll also need to replace the example secret key with
-any random string of letters and numbers: a keyboard mash will do.
-
-## Set Up HTTPS
-
-The OAuth protocol requires that all communication occur via the secure HTTPS
-connections, rather than insecure HTTP connections. There are several ways
-to set up HTTPS on your computer, but perhaps the simplest is to use
-[ngrok](https://ngrok.com), a tool that lets you create a secure tunnel
-from the ngrok website to your computer. Install it from the website, and
-then run the following command:
-
-```
-ngrok http 5000
-```
-
-Notice that ngrok will show you two "forwarding" URLs, which may look something
-like `http://ed90abe7.ngrok.io` and `https://ed90abe7.ngrok.io`. (The hash
-subdomain will be different for you.) You'll be using the second URL, which
-starts with `https`.
-
-Alternatively, you can set the `OAUTHLIB_INSECURE_TRANSPORT` environment
-variable in your shell, to disable the HTTPS check. That way, you'll be
-able to use `localhost` to refer to your app, instead of an ngrok URL.
-However, be aware that you won't be able to do this when you deploy
-your app to production, so it's usually a better idea to set up HTTPS properly.
-
-## Set the Nylas Callback URL
-
-Once you have a HTTPS URL that points to your computer, you'll need to tell
-Nylas about it. On the [Nylas Dashboard](https://dashboard.nylas.com)
-click on the Application Dropdown Menu on the left, then "View all Applications".
-From there, select "Edit" for the app you'd like to use and select the
-"Application Callbacks" tab. Paste your HTTPS URL into the text field, and add
-`/login/nylas/authorized` after it. For example, if your HTTPS URL is
-`https://ad172180.ngrok.io`, then you would put `https://ad172180.ngrok.io/login/nylas/authorized`
-into the text field in the "Application Callbacks" tab.
-
-Then click the "Add Callback" button to save.
-
-## Install the Dependencies
-
-This project depends on a few third-party Python modules, like Flask.
-These dependencies are listed in the `requirements.txt` file in this directory.
-To install them, use the `pip` tool, like this:
-
-```
-pip install -r requirements.txt
-```
-
-## Run the Example
-
-Finally, run the example project like this:
-
-```
-python server.py
-```
-
-Once the server is running, visit the ngrok URL in your browser to test it out!
diff --git a/examples/hosted-oauth/config.json b/examples/hosted-oauth/config.json
deleted file mode 100644
index 9b54e3df..00000000
--- a/examples/hosted-oauth/config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "SECRET_KEY": "replace me with a random string",
- "NYLAS_OAUTH_CLIENT_ID": "replace me with the client ID from Nylas",
- "NYLAS_OAUTH_CLIENT_SECRET": "replace me with the client secret from Nylas"
-}
diff --git a/examples/hosted-oauth/requirements.txt b/examples/hosted-oauth/requirements.txt
deleted file mode 100644
index e87c213b..00000000
--- a/examples/hosted-oauth/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Flask>=0.11
-Flask-Dance>=0.11.1
-requests
diff --git a/examples/hosted-oauth/server.py b/examples/hosted-oauth/server.py
deleted file mode 100644
index 72caed31..00000000
--- a/examples/hosted-oauth/server.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# Imports from the Python standard library
-from __future__ import print_function
-import os
-import sys
-import textwrap
-
-# Imports from third-party modules that this project depends on
-try:
- import requests
- from flask import Flask, render_template
- from werkzeug.middleware.proxy_fix import ProxyFix
- from flask_dance.contrib.nylas import make_nylas_blueprint, nylas
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the dependencies for this project.
- To do so, run this command:
-
- pip install -r requirements.txt
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-try:
- from nylas import APIClient
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the Nylas SDK for this project.
- To do so, run this command:
-
- pip install nylas
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# This example uses Flask, a micro web framework written in Python.
-# For more information, check out the documentation: http://flask.pocoo.org
-# Create a Flask app, and load the configuration file.
-app = Flask(__name__)
-app.config.from_json("config.json")
-
-# Check for dummy configuration values.
-# If you are building your own application based on this example,
-# you can remove this check from your code.
-cfg_needs_replacing = [
- key
- for key, value in app.config.items()
- if isinstance(value, str) and value.startswith("replace me")
-]
-if cfg_needs_replacing:
- message = textwrap.dedent(
- """
- This example will only work if you replace the fake configuration
- values in `config.json` with real configuration values.
- The following config values need to be replaced:
- {keys}
- Consult the README.md file in this directory for more information.
- """
- ).format(keys=", ".join(cfg_needs_replacing))
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# Use Flask-Dance to automatically set up the OAuth endpoints for Nylas.
-# For more information, check out the documentation: http://flask-dance.rtfd.org
-nylas_bp = make_nylas_blueprint()
-app.register_blueprint(nylas_bp, url_prefix="/login")
-
-# Teach Flask how to find out that it's behind an ngrok proxy
-app.wsgi_app = ProxyFix(app.wsgi_app)
-
-# Define what Flask should do when someone visits the root URL of this website.
-@app.route("/")
-def index():
- # If the user has already connected to Nylas via OAuth,
- # `nylas.authorized` will be True. Otherwise, it will be False.
- if not nylas.authorized:
- # OAuth requires HTTPS. The template will display a handy warning,
- # unless we've overridden the check.
- return render_template(
- "before_authorized.html",
- insecure_override=os.environ.get("OAUTHLIB_INSECURE_TRANSPORT"),
- )
-
- # If we've gotten to this point, then the user has already connected
- # to Nylas via OAuth. Let's set up the SDK client with the OAuth token:
- client = APIClient(
- client_id=app.config["NYLAS_OAUTH_CLIENT_ID"],
- client_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"],
- access_token=nylas.access_token,
- )
-
- # We'll use the Nylas client to fetch information from Nylas
- # about the current user, and pass that to the template.
- account = client.account
- return render_template("after_authorized.html", account=account)
-
-
-def ngrok_url():
- """
- If ngrok is running, it exposes an API on port 4040. We can use that
- to figure out what URL it has assigned, and suggest that to the user.
- https://ngrok.com/docs#list-tunnels
- """
- try:
- ngrok_resp = requests.get("http://localhost:4040/api/tunnels")
- except requests.ConnectionError:
- # I guess ngrok isn't running.
- return None
- ngrok_data = ngrok_resp.json()
- secure_urls = [
- tunnel["public_url"]
- for tunnel in ngrok_data["tunnels"]
- if tunnel["proto"] == "https"
- ]
- return secure_urls[0]
-
-
-# When this file is executed, run the Flask web server.
-if __name__ == "__main__":
- url = ngrok_url()
- if url:
- print(" * Visit {url} to view this Nylas example".format(url=url))
-
- app.run()
diff --git a/examples/hosted-oauth/templates/after_authorized.html b/examples/hosted-oauth/templates/after_authorized.html
deleted file mode 100644
index 04e010a0..00000000
--- a/examples/hosted-oauth/templates/after_authorized.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-
You've successfully connected to Nylas via hosted OAuth! Here's some
- information that I got from the Nylas API, to prove it:
-
-
- {% for key, value in account.items() %}
-
-
{{ key }}
-
{{ value }}
-
- {% endfor %}
-
-
-
If you want to test this OAuth flow again, clear your browser cookies
- or open a new browser in incognito mode.
-
-
diff --git a/examples/hosted-oauth/templates/before_authorized.html b/examples/hosted-oauth/templates/before_authorized.html
deleted file mode 100644
index 9a7215ba..00000000
--- a/examples/hosted-oauth/templates/before_authorized.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-{% if not insecure_override %}
-
-
Warning
-
OAuth requires HTTPS, and this page is loaded via insecure HTTP.
- If you try to connect to Nylas like this, it will fail.
- To fix this problem, serve this website over HTTPS.
-
-
Since this is just an example, you can disable this requirement by
- setting the OAUTHLIB_INSECURE_TRANSPORT environment
- variable to 1 before running this example again.
- However, you will not be able to do this when running
- in production.
-
-
-{% endif %}
-
-
Thanks for giving Nylas a try! Next, you need to click this button
- to connect to Nylas via hosted OAuth.
-Connect to Nylas
-
-
-{% endblock %}
diff --git a/examples/import_events_demo/README.md b/examples/import_events_demo/README.md
new file mode 100644
index 00000000..cb52af00
--- /dev/null
+++ b/examples/import_events_demo/README.md
@@ -0,0 +1,68 @@
+# Import Events Demo
+
+This example demonstrates the usage of the `list_import_events` method in the Nylas SDK. This method returns a list of recurring events, recurring event exceptions, and single events from a specified calendar within a given time frame. It's particularly useful when you want to import, store, and synchronize events from a calendar to your application.
+
+## Features Demonstrated
+
+1. **Basic Usage**: Shows how to use `list_import_events` with required parameters.
+2. **Time Filtering**: Demonstrates filtering events by start and end time.
+3. **Pagination**: Shows how to handle paginated results with `limit` and `page_token`.
+4. **Field Selection**: Demonstrates how to use the `select` parameter to request only specific fields.
+5. **Multiple Scenarios**: Shows various parameter combinations for different use cases.
+
+## Setup
+
+1. Create a `.env` file in the root directory with your Nylas API credentials:
+ ```
+ NYLAS_API_KEY=your_api_key_here
+ NYLAS_GRANT_ID=your_grant_id_here
+ ```
+
+2. Install the required dependencies:
+ ```bash
+ pip install nylas python-dotenv
+ ```
+
+## Running the Example
+
+Run the example script:
+```bash
+python examples/import_events_demo/import_events_example.py
+```
+
+The script will demonstrate different ways to use the `list_import_events` method with various parameters.
+
+## Example Output
+
+The script will show output similar to this:
+```
+=== Import Events Demo ===
+
+Basic import (primary calendar):
+Event - Title: Team Meeting, ID: abc123...
+
+Time-filtered import (Jan 1, 2023 - Dec 31, 2023):
+Event - Title: Annual Review, ID: def456...
+
+Limited results with field selection (only id, title and when):
+Event - Title: Client Call, ID: ghi789...
+```
+
+## Benefits of Using Import Events
+
+1. **Efficient Syncing**: Easily synchronize calendar events to your application or database.
+2. **Better Performance**: Using time filters and limiting results can improve performance.
+3. **Selective Data**: Using the select parameter allows you to request only the fields you need.
+
+## Available Parameters
+
+The `list_import_events` method accepts the following parameters:
+
+- `calendar_id` (required): Specify the calendar ID to import events from. You can use "primary" for the user's primary calendar.
+- `start`: Filter for events starting at or after this Unix timestamp.
+- `end`: Filter for events ending at or before this Unix timestamp.
+- `select`: Comma-separated list of fields to return in the response.
+- `limit`: Maximum number of objects to return (defaults to 50, max 200).
+- `page_token`: Token for retrieving the next page of results.
+
+For more information, refer to the [Nylas API documentation](https://developer.nylas.com/).
\ No newline at end of file
diff --git a/examples/import_events_demo/import_events_example.py b/examples/import_events_demo/import_events_example.py
new file mode 100644
index 00000000..da347e60
--- /dev/null
+++ b/examples/import_events_demo/import_events_example.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using Import Events
+
+This example demonstrates how to use the 'list_import_events' method to import and
+synchronize events from a calendar within a given time frame.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/import_events_demo/import_events_example.py
+"""
+
+import os
+import sys
+import json
+import time
+from datetime import datetime, timedelta
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_data(data: list, title: str) -> None:
+ """Pretty print the data with a title."""
+ print(f"\n{title}:")
+ for item in data:
+ # Convert to dict and pretty print
+ item_dict = item.to_dict()
+ print(json.dumps(item_dict, indent=2))
+
+
+def demonstrate_basic_import(client: Client, grant_id: str) -> None:
+ """Demonstrate basic usage of list_import_events with primary calendar."""
+ print("\n=== Basic Import Events ===")
+
+ print("\nFetching events from primary calendar:")
+ events = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={"calendar_id": "primary", "limit": 2}
+ )
+ print_data(events.data, "Basic import events")
+
+
+def demonstrate_time_filtered_import(client: Client, grant_id: str) -> None:
+ """Demonstrate import events with time filtering."""
+ print("\n=== Time Filtered Import Events ===")
+
+ # Get timestamps for a one-month period
+ now = int(time.time())
+ one_month_ago = now - (30 * 24 * 60 * 60) # 30 days ago
+ one_month_future = now + (30 * 24 * 60 * 60) # 30 days in future
+
+ # Format dates for display
+ from_date = datetime.fromtimestamp(one_month_ago).strftime("%Y-%m-%d")
+ to_date = datetime.fromtimestamp(one_month_future).strftime("%Y-%m-%d")
+
+ print(f"\nFetching events from {from_date} to {to_date}:")
+ events = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "start": one_month_ago,
+ "end": one_month_future
+ }
+ )
+ print_data(events.data, f"Events from {from_date} to {to_date}")
+
+
+def demonstrate_limit(client: Client, grant_id: str) -> None:
+ """Demonstrate import events with limit parameter."""
+ print("\n=== Import Events with Max Results ===")
+
+ print("\nFetching events with limit=5:")
+ events = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "limit": 5
+ }
+ )
+ print_data(events.data, "Events with limit=5")
+
+
+def demonstrate_field_selection(client: Client, grant_id: str) -> None:
+ """Demonstrate import events with field selection."""
+ print("\n=== Import Events with Field Selection ===")
+
+ print("\nFetching events with select parameter (only id, title, and when):")
+ events = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "limit": 2,
+ "select": "id,title,when"
+ }
+ )
+ print_data(events.data, "Events with selected fields only")
+
+
+def demonstrate_pagination(client: Client, grant_id: str) -> None:
+ """Demonstrate pagination for import events."""
+ print("\n=== Import Events with Pagination ===")
+
+ # First page
+ print("\nFetching first page of events (limit=3):")
+ first_page = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "limit": 3
+ }
+ )
+ print_data(first_page.data, "First page of events")
+
+ # If there's a next page, fetch it
+ if hasattr(first_page, 'next_cursor') and first_page.next_cursor:
+ print("\nFetching second page of events:")
+ second_page = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "limit": 3,
+ "page_token": first_page.next_cursor
+ }
+ )
+ print_data(second_page.data, "Second page of events")
+ else:
+ print("\nNo second page available - not enough events to paginate")
+
+
+def demonstrate_full_example(client: Client, grant_id: str) -> None:
+ """Demonstrate a full example with all parameters."""
+ print("\n=== Full Import Events Example ===")
+
+ # Get timestamps for the current year
+ now = datetime.now()
+ start_of_year = datetime(now.year, 1, 1).timestamp()
+ end_of_year = datetime(now.year, 12, 31, 23, 59, 59).timestamp()
+
+ print(f"\nFetching events for {now.year} with all parameters:")
+ events = client.events.list_import_events(
+ identifier=grant_id,
+ query_params={
+ "calendar_id": "primary",
+ "limit": 10,
+ "start": int(start_of_year),
+ "end": int(end_of_year),
+ "select": "id,title,description,when,participants,location"
+ }
+ )
+ print_data(events.data, f"Events for {now.year} with all parameters")
+
+
+def main():
+ """Main function demonstrating the import events method."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(
+ api_key=api_key,
+ )
+
+ print("\nDemonstrating Import Events Functionality")
+ print("========================================")
+
+ # Demonstrate different ways to use list_import_events
+ demonstrate_basic_import(client, grant_id)
+ demonstrate_time_filtered_import(client, grant_id)
+ demonstrate_limit(client, grant_id)
+ demonstrate_field_selection(client, grant_id)
+ demonstrate_pagination(client, grant_id)
+ demonstrate_full_example(client, grant_id)
+
+ print("\nExample completed!")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/include_hidden_folders_demo/README.md b/examples/include_hidden_folders_demo/README.md
new file mode 100644
index 00000000..dc1b99aa
--- /dev/null
+++ b/examples/include_hidden_folders_demo/README.md
@@ -0,0 +1,67 @@
+# Include Hidden Folders Example
+
+This example demonstrates how to use the `include_hidden_folders` query parameter when listing folders with the Nylas Python SDK.
+
+## Overview
+
+The `include_hidden_folders` parameter is Microsoft-specific and allows you to include hidden folders in the folder listing response. By default, this parameter is `False` and hidden folders are not included.
+
+## Prerequisites
+
+1. A Nylas application with Microsoft OAuth configured
+2. A valid Nylas API key
+3. A grant ID for a Microsoft account
+
+## Setup
+
+1. Set your environment variables:
+ ```bash
+ export NYLAS_API_KEY="your_api_key_here"
+ export NYLAS_GRANT_ID="your_grant_id_here"
+ export NYLAS_API_URI="https://api.us.nylas.com" # Optional, defaults to US
+ ```
+
+2. Install the Nylas Python SDK:
+ ```bash
+ pip install nylas
+ ```
+
+## Running the Example
+
+```bash
+python include_hidden_folders_example.py
+```
+
+## Code Explanation
+
+The example demonstrates two scenarios:
+
+1. **Default behavior**: Lists folders without hidden folders
+ ```python
+ folders_response = nylas.folders.list(
+ identifier=grant_id,
+ query_params={"limit": 10}
+ )
+ ```
+
+2. **With hidden folders**: Lists folders including hidden folders (Microsoft only)
+ ```python
+ folders_with_hidden_response = nylas.folders.list(
+ identifier=grant_id,
+ query_params={
+ "include_hidden_folders": True,
+ "limit": 10
+ }
+ )
+ ```
+
+## Expected Output
+
+The example will show:
+- List of regular folders
+- List of folders including hidden ones (if any)
+- Comparison showing how many additional hidden folders were found
+
+## Note
+
+This feature is **Microsoft-specific only**. For other providers (Google, IMAP), the `include_hidden_folders` parameter will be ignored.
\ No newline at end of file
diff --git a/examples/include_hidden_folders_demo/include_hidden_folders_example.py b/examples/include_hidden_folders_demo/include_hidden_folders_example.py
new file mode 100644
index 00000000..39f2226e
--- /dev/null
+++ b/examples/include_hidden_folders_demo/include_hidden_folders_example.py
@@ -0,0 +1,71 @@
+import os
+from nylas import Client
+
+
+def main():
+ """
+ This example demonstrates how to use the include_hidden_folders parameter
+ when listing folders with the Nylas SDK.
+
+ The include_hidden_folders parameter is Microsoft-specific and when set to True,
+ it includes hidden folders in the response.
+ """
+
+ # Initialize the client
+ nylas = Client(
+ api_key=os.environ.get("NYLAS_API_KEY"),
+ api_uri=os.environ.get("NYLAS_API_URI", "https://api.us.nylas.com"),
+ )
+
+ # Get the grant ID from environment variable
+ grant_id = os.environ.get("NYLAS_GRANT_ID")
+
+ if not grant_id:
+ print("Please set the NYLAS_GRANT_ID environment variable")
+ return
+
+ try:
+ print("Listing folders without hidden folders (default behavior):")
+ print("=" * 60)
+
+ # List folders without hidden folders (default)
+ folders_response = nylas.folders.list(
+ identifier=grant_id, query_params={"limit": 10}
+ )
+
+ for folder in folders_response.data:
+ print(f"- {folder.name} (ID: {folder.id})")
+
+ print(f"\nTotal folders found: {len(folders_response.data)}")
+
+ # Now list folders WITH hidden folders (Microsoft only)
+ print("\n\nListing folders with hidden folders included (Microsoft only):")
+ print("=" * 70)
+
+ folders_with_hidden_response = nylas.folders.list(
+ identifier=grant_id,
+ query_params={"include_hidden_folders": True, "limit": 10},
+ )
+
+ for folder in folders_with_hidden_response.data:
+ print(f"- {folder.name} (ID: {folder.id})")
+
+ print(
+ f"\nTotal folders found (including hidden): {len(folders_with_hidden_response.data)}"
+ )
+
+ # Compare the counts
+ hidden_count = len(folders_with_hidden_response.data) - len(
+ folders_response.data
+ )
+ if hidden_count > 0:
+ print(f"\nFound {hidden_count} additional hidden folder(s)")
+ else:
+ print("\nNo additional hidden folders found")
+
+ except Exception as e:
+ print(f"Error: {e}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/inline_attachment_demo/README.md b/examples/inline_attachment_demo/README.md
new file mode 100644
index 00000000..5baaca0d
--- /dev/null
+++ b/examples/inline_attachment_demo/README.md
@@ -0,0 +1,65 @@
+# Inline Attachment Example
+
+This example demonstrates how to send messages and drafts with inline attachments using the `content_id` field in the Nylas Python SDK.
+
+## What This Example Shows
+
+- How to create inline attachments with `content_id` for HTML emails
+- How the SDK properly handles `content_id` for large attachments (>3MB)
+- The difference between inline attachments and regular attachments
+- How to reference inline attachments in HTML email bodies using `cid:` syntax
+
+## Key Features Demonstrated
+
+### Content ID Usage
+When an attachment includes a `content_id` field, the SDK will use this as the field name in multipart form data instead of the generic `file{index}` pattern. This is crucial for inline attachments that need to be referenced in the email body.
+
+### HTML Email with Inline Images
+The example shows how to:
+1. Set the `content_id` field in the attachment
+2. Reference the attachment in HTML using `src="cid:your-content-id"`
+3. Set appropriate inline properties (`is_inline: True`, `content_disposition: "inline"`)
+
+### Large Attachment Handling
+For attachments larger than 3MB, the SDK automatically switches from JSON to multipart form data. With this fix, the `content_id` is now properly respected in the form field names.
+
+## Running the Example
+
+1. Set your Nylas API key:
+ ```bash
+ export NYLAS_API_KEY='your-api-key-here'
+ ```
+
+2. Update the grant ID and email addresses in the script
+
+3. Run the example:
+ ```bash
+ python inline_attachment_example.py
+ ```
+
+## Important Notes
+
+- **Content ID Format**: Use a unique identifier for each inline attachment (e.g., `"image1@example.com"`, `"logo"`, `"banner-image"`)
+- **HTML Reference**: Reference inline attachments in HTML using `src="cid:your-content-id"`
+- **Backward Compatibility**: Attachments without `content_id` still work as before using `file{index}` naming
+- **File Size Threshold**: The 3MB threshold determines whether JSON or form data is used for the request
+
+## Expected Behavior
+
+### Before the Fix (Problematic)
+```
+Form data fields:
+- message: (JSON payload)
+- file0: (inline image - content_id ignored)
+- file1: (regular attachment)
+```
+
+### After the Fix (Correct)
+```
+Form data fields:
+- message: (JSON payload)
+- my-inline-image: (inline image - uses content_id)
+- file1: (regular attachment - fallback to file{index})
+```
+
+This ensures that email clients can properly display inline images by matching the `content_id` in the HTML `cid:` reference with the multipart form field name.
diff --git a/examples/inline_attachment_demo/inline_attachment_example.py b/examples/inline_attachment_demo/inline_attachment_example.py
new file mode 100644
index 00000000..21fc1bc0
--- /dev/null
+++ b/examples/inline_attachment_demo/inline_attachment_example.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python3
+
+import base64
+import io
+import os
+from nylas import Client
+
+
+def send_message_with_inline_attachment():
+ """
+ This example demonstrates how to send a message with an inline attachment
+ that uses a content_id for referencing in HTML email bodies.
+
+ This is particularly useful for embedding images directly in HTML emails
+ where the image is referenced using 'cid:' in the src attribute.
+ """
+
+ # Initialize the Nylas client
+ nylas = Client(
+ api_key=os.environ.get("NYLAS_API_KEY"), # Replace with your API key
+ )
+
+ # Get test email
+ test_email = os.environ.get("TEST_EMAIL")
+
+ # Get grant
+ grant_id = os.environ.get("NYLAS_GRANT_ID")
+
+ # Create a sample image content using base64 decoded data
+ # This is a small PNG image that can be used for demonstration
+ base64_image = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEQUlEQVRYhe2WW2wUVRyHv5nZS7vb3XbLdrfXjSZ9AxOqDRANGjDRSKLRKoIYNLENpEgJRQIKKvSi0tI28oImhtISqg9cTEzUhIgmSkAQY4UWQ2yALi3d7S4UaPcyO7MzPkwlJXG3S02sD5yH8zLJ//vO7/zPmSMsqR0MMIvDBHhnU0CcTfh9gf+FgOnfFlCTIKs6mg6iABaTgFn6DwQ0DUYnNBaVW3iyMpvcHImJmMaPvTG+75PxOkSkDPKdkUBC1bFZRb76sIhyn/WubyufyePaqEJdW5DAWBKrWUhb6557QFZ13E6Jb/eUUe6zcuzkOMu3DlG0/DLPbrrKl8dvUewxc6S1lJI5EqqWvp6wpHZQzxiu6HjyJI60lCKKsLkjwI1xjfdr3PiKLYyMKnQcvE7oVpIDjSWEx1QW1w1R5Ey9zowTkBUd7xR4fVsAm1Wgc0cxD5RY6P8zRrHHTPtbhZS6TXzxzU3cLhNVC7NJqKnXmJGArOgUuiQOtxrwjbsDOG0ijW96kRM6T2+4yorGIAuqB1FUnc2vu9lz9BYAC+ZmkVBT155WQFZ0ilwSh1tKEQXY0DqCK0ekYZ2HeEJn6Xo/t6MahU6RuKJztj9Kfq7EwA1j8y1mkXR7nFZASeoU50scai1FEKCuZQS3U2JHrYe4rLGgZpDmmjn4CiRkVcdmFaica2PsdpLyfKN0QtFIdw5SHkNdB6tZ4FBLKQDrd43gdUm8t9ZDTNaorPbzycYCHq+0Y8sSWdMe4sTeMswmgd1dYTZU5QJwpj+OJc1hT5uApsGAX6Zu1wiF+ZPwuMbD1X4+rTfgv5yPUtUU5MTeMhx2kY4DYQaDKquW5REeUzl6OobFlDqDlAKCYKSwuP4aZV4T767xEI1rVFT7+WxTAYsfsXPmfJTnGoL07fPhsIu0d4c5e1Gm54MSANa1BCiwp2+ztDfheFxny/MO6le7icY0Kmr8dG4u4LEKO6fPRXmhKciFTh8up8TurjC9AzKfT8Jf3T7M0PUkWdPchCkFlCQ8Mc9K/Wo3kZix8q4tBTw6387Pv0d4sXmUC50+8hwSrftDnLuUoKfZgK/aPszlUZXsaeCQZgskEa4EVH76NcKitX66txrwU70RXpoCb+kM0XcpwcEmA/7KtmGuZAhPm4AoQHhcY1lTkO+aCln4kI2Tv0VY8dEo/ft95OZI7NoX4o/BBAcm4Su3DeMPqdPGnpEAQHhC44fmQirnGfA32kL0d/pw5hixX7qm0N1owFe8M8TV8PR7nrFARNZpeM1lwHsjPLUzyLGdXpw5Em3dYT7+eoLjrUUAvPz2UEYN908j5d8woeosnZ/N3ActNPTcxOMQybOJlHkkTl1M4MgSUJNGryQ1HbN07/C0An9LKEmwW43img5JjTtPLn1yEmbGBqbpAYtJuOsaFQUQp7z3hDvTzMesv4rvC8y6gAmIzKbAX+u0pDGsEb6KAAAAAElFTkSuQmCC"
+ image_content = base64.b64decode(base64_image)
+
+ # Create the message with inline attachment
+ message_request = {
+ "to": [{"email": test_email, "name": "Recipient Name"}],
+ "from": [{"email": test_email, "name": "Sender Name"}],
+ "subject": "Message with Inline Image",
+ "body": """
+
+
+
Hello!
+
This email contains an inline image:
+
+
The image above is embedded directly in the email using content_id.
+
+
+ """,
+ "attachments": [
+ {
+ "filename": "inline-image.png",
+ "content_type": "image/png",
+ "content": io.BytesIO(image_content),
+ "size": len(image_content),
+ "content_id": "my-inline-image", # This is the key for inline attachments
+ "is_inline": True,
+ "content_disposition": "inline"
+ },
+ {
+ # Regular attachment without content_id for comparison
+ "filename": "regular-attachment.txt",
+ "content_type": "text/plain",
+ "content": io.BytesIO(b"This is a regular attachment"),
+ "size": 28,
+ # No content_id - this will use the default file{index} naming
+ }
+ ]
+ }
+
+ try:
+ # Send the message
+ response = nylas.messages.send(
+ identifier=grant_id, # Replace with your grant ID
+ request_body=message_request
+ )
+
+ print("Message sent successfully!")
+ print(f"Message ID: {response.data.id}")
+ print(f"Thread ID: {response.data.thread_id}")
+
+ # The inline attachment will be referenced by its content_id in the form data
+ # instead of a generic file{index} name, allowing proper inline display
+
+ except Exception as e:
+ print(f"Error sending message: {e}")
+
+
+def send_draft_with_inline_attachment():
+ """
+ This example demonstrates how to create and send a draft with an inline attachment.
+ """
+
+ # Initialize the Nylas client
+ nylas = Client(
+ api_key=os.environ.get("NYLAS_API_KEY"), # Replace with your API key
+ )
+
+ # Get test email
+ test_email = os.environ.get("TEST_EMAIL")
+
+ # Get grant
+ grant_id = os.environ.get("NYLAS_GRANT_ID")
+
+ # Create a larger image content to trigger form data usage (>3MB threshold)
+ # For demo purposes, we'll replicate the same image data multiple times
+ # In real usage, large images would automatically use the content_id functionality
+ base64_image = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEQUlEQVRYhe2WW2wUVRyHv5nZS7vb3XbLdrfXjSZ9AxOqDRANGjDRSKLRKoIYNLENpEgJRQIKKvSi0tI28oImhtISqg9cTEzUhIgmSkAQY4UWQ2yALi3d7S4UaPcyO7MzPkwlJXG3S02sD5yH8zLJ//vO7/zPmSMsqR0MMIvDBHhnU0CcTfh9gf+FgOnfFlCTIKs6mg6iABaTgFn6DwQ0DUYnNBaVW3iyMpvcHImJmMaPvTG+75PxOkSkDPKdkUBC1bFZRb76sIhyn/WubyufyePaqEJdW5DAWBKrWUhb6557QFZ13E6Jb/eUUe6zcuzkOMu3DlG0/DLPbrrKl8dvUewxc6S1lJI5EqqWvp6wpHZQzxiu6HjyJI60lCKKsLkjwI1xjfdr3PiKLYyMKnQcvE7oVpIDjSWEx1QW1w1R5Ey9zowTkBUd7xR4fVsAm1Wgc0cxD5RY6P8zRrHHTPtbhZS6TXzxzU3cLhNVC7NJqKnXmJGArOgUuiQOtxrwjbsDOG0ijW96kRM6T2+4yorGIAuqB1FUnc2vu9lz9BYAC+ZmkVBT155WQFZ0ilwSh1tKEQXY0DqCK0ekYZ2HeEJn6Xo/t6MahU6RuKJztj9Kfq7EwA1j8y1mkXR7nFZASeoU50scai1FEKCuZQS3U2JHrYe4rLGgZpDmmjn4CiRkVcdmFaica2PsdpLyfKN0QtFIdw5SHkNdB6tZ4FBLKQDrd43gdUm8t9ZDTNaorPbzycYCHq+0Y8sSWdMe4sTeMswmgd1dYTZU5QJwpj+OJc1hT5uApsGAX6Zu1wiF+ZPwuMbD1X4+rTfgv5yPUtUU5MTeMhx2kY4DYQaDKquW5REeUzl6OobFlDqDlAKCYKSwuP4aZV4T767xEI1rVFT7+WxTAYsfsXPmfJTnGoL07fPhsIu0d4c5e1Gm54MSANa1BCiwp2+ztDfheFxny/MO6le7icY0Kmr8dG4u4LEKO6fPRXmhKciFTh8up8TurjC9AzKfT8Jf3T7M0PUkWdPchCkFlCQ8Mc9K/Wo3kZix8q4tBTw6387Pv0d4sXmUC50+8hwSrftDnLuUoKfZgK/aPszlUZXsaeCQZgskEa4EVH76NcKitX66txrwU70RXpoCb+kM0XcpwcEmA/7KtmGuZAhPm4AoQHhcY1lTkO+aCln4kI2Tv0VY8dEo/ft95OZI7NoX4o/BBAcm4Su3DeMPqdPGnpEAQHhC44fmQirnGfA32kL0d/pw5hixX7qm0N1owFe8M8TV8PR7nrFARNZpeM1lwHsjPLUzyLGdXpw5Em3dYT7+eoLjrUUAvPz2UEYN908j5d8woeosnZ/N3ActNPTcxOMQybOJlHkkTl1M4MgSUJNGryQ1HbN07/C0An9LKEmwW43img5JjTtPLn1yEmbGBqbpAYtJuOsaFQUQp7z3hDvTzMesv4rvC8y6gAmIzKbAX+u0pDGsEb6KAAAAAElFTkSuQmCC"
+ large_image_content = base64.b64decode(base64_image) * 1000 # Replicated to make it large
+
+ # Create the draft with inline attachment
+ draft_request = {
+ "to": [{"email": test_email, "name": "Recipient Name"}],
+ "from": [{"email": test_email, "name": "Sender Name"}],
+ "subject": "Draft with Inline Image",
+ "body": """
+
+
+
Draft Email
+
This draft contains an inline image:
+
+
Best regards, Your Team
+
+
+ """,
+ "attachments": [
+ {
+ "filename": "company-logo.png",
+ "content_type": "image/png",
+ "content": io.BytesIO(large_image_content),
+ "size": len(large_image_content),
+ "content_id": "logo-image", # Content ID for inline reference
+ "is_inline": True,
+ "content_disposition": "inline"
+ }
+ ]
+ }
+
+ try:
+ # Create the draft
+ draft_response = nylas.drafts.create(
+ identifier=grant_id, # Replace with your grant ID
+ request_body=draft_request
+ )
+
+ print("Draft created successfully!")
+ print(f"Draft ID: {draft_response.data.id}")
+
+ # Send the draft
+ send_response = nylas.drafts.send(
+ identifier=grant_id, # Replace with your grant ID
+ draft_id=draft_response.data.id
+ )
+
+ print("Draft sent successfully!")
+ print(f"Message ID: {send_response.data.id}")
+
+ except Exception as e:
+ print(f"Error with draft: {e}")
+
+
+if __name__ == "__main__":
+ print("Inline Attachment Example")
+ print("=" * 50)
+ print()
+
+ # Check if API key is set
+ if not os.environ.get("NYLAS_API_KEY"):
+ print("Please set the NYLAS_API_KEY environment variable")
+ print("export NYLAS_API_KEY='your-api-key-here'")
+ exit(1)
+
+ # Check if grant ID is set
+ if not os.environ.get("NYLAS_GRANT_ID"):
+ print("Please set the NYLAS_GRANT_ID environment variable")
+ print("export NYLAS_GRANT_ID='your-grant-id-here'")
+ exit(1)
+
+ # Check if test email is set
+ if not os.environ.get("TEST_EMAIL"):
+ print("Please set the TEST_EMAIL environment variable")
+ print("export TEST_EMAIL='your-test-email-here'")
+ exit(1)
+
+ print("1. Sending message with inline attachment...")
+ send_message_with_inline_attachment()
+
+ print("\n2. Creating and sending draft with inline attachment...")
+ send_draft_with_inline_attachment()
+
+ print("\nNote: The content_id field ensures that large inline attachments")
+ print("are properly referenced in the multipart form data, allowing")
+ print("email clients to display them inline correctly.")
diff --git a/examples/is_plaintext_demo/README.md b/examples/is_plaintext_demo/README.md
new file mode 100644
index 00000000..da0d31b1
--- /dev/null
+++ b/examples/is_plaintext_demo/README.md
@@ -0,0 +1,156 @@
+# is_plaintext Demo
+
+This example demonstrates the usage of the new `is_plaintext` property for messages and drafts in the Nylas API. This property controls whether message content is sent as plain text or HTML in the MIME data.
+
+## Features Demonstrated
+
+1. **Plain Text Messages**: Shows how to send messages with `is_plaintext=True` to send content as plain text without HTML in MIME data.
+2. **HTML Messages**: Demonstrates sending messages with `is_plaintext=False` to include HTML formatting in MIME data.
+3. **Backwards Compatibility**: Shows that existing code continues to work without specifying the `is_plaintext` property.
+4. **Draft Operations**: Demonstrates using `is_plaintext` with draft creation and updates.
+
+## API Property Overview
+
+### is_plaintext
+
+- **Type**: `boolean`
+- **Default**: `false`
+- **Available in**:
+ - `messages.send()` - Send message endpoint
+ - `drafts.create()` - Create draft endpoint
+ - `drafts.update()` - Update draft endpoint
+
+When `is_plaintext` is:
+- `true`: The message body is sent as plain text and the MIME data doesn't include the HTML version of the message
+- `false`: The message body is sent as HTML and MIME data includes HTML formatting
+- Not specified: Uses API default behavior (same as `false`)
+
+## Setup
+
+1. Install the SDK in development mode from the repository root:
+```bash
+cd /path/to/nylas-python
+pip install -e .
+```
+
+2. Set your environment variables:
+```bash
+export NYLAS_API_KEY="your_api_key"
+export NYLAS_GRANT_ID="your_grant_id"
+```
+
+3. Run the example from the repository root:
+```bash
+python examples/is_plaintext_demo/is_plaintext_example.py
+```
+
+## Example Output
+
+```
+Demonstrating is_plaintext Property Usage
+=======================================
+
+=== Sending Plain Text Message ===
+Sending message with is_plaintext=True...
+โ Message request prepared with is_plaintext=True
+๐ง Message configured to be sent as plain text (MIME without HTML version)
+
+=== Sending HTML Message ===
+Sending message with is_plaintext=False (HTML)...
+โ Message request prepared with is_plaintext=False
+๐ Message configured to be sent as HTML (MIME includes HTML version)
+
+=== Backwards Compatibility (No is_plaintext specified) ===
+Sending message without is_plaintext property...
+โ Existing code continues to work without modification
+
+=== Creating Plain Text Draft ===
+Creating draft with is_plaintext=True...
+โ Draft request prepared with is_plaintext=True
+๐ Draft configured to be sent as plain text when sent
+
+Example completed successfully!
+```
+
+## Use Cases
+
+### Plain Text (is_plaintext=true)
+- **Simple Notifications**: System alerts, password resets, account confirmations
+- **Text-Only Emails**: Newsletters or announcements that don't need formatting
+- **Lightweight Messaging**: Reduce message size and improve compatibility
+- **Accessibility**: Better support for screen readers and text-only email clients
+
+### HTML (is_plaintext=false)
+- **Marketing Emails**: Rich formatting, images, and branded content
+- **Newsletters**: Complex layouts with multiple sections and styling
+- **Transactional Emails**: Formatted receipts, invoices, and reports
+- **Interactive Content**: Buttons, links, and styled call-to-action elements
+
+## Code Examples
+
+### Send Plain Text Message
+```python
+message_request = {
+ "to": [{"email": "user@example.com", "name": "User"}],
+ "subject": "Plain Text Notification",
+ "body": "This is a plain text message.",
+ "is_plaintext": True
+}
+
+response = client.messages.send(
+ identifier=grant_id,
+ request_body=message_request
+)
+```
+
+### Send HTML Message
+```python
+message_request = {
+ "to": [{"email": "user@example.com", "name": "User"}],
+ "subject": "HTML Newsletter",
+ "body": "
Welcome!
This is HTML content.
",
+ "is_plaintext": False
+}
+
+response = client.messages.send(
+ identifier=grant_id,
+ request_body=message_request
+)
+```
+
+### Create Plain Text Draft
+```python
+draft_request = {
+ "to": [{"email": "user@example.com", "name": "User"}],
+ "subject": "Draft Message",
+ "body": "This draft will be sent as plain text.",
+ "is_plaintext": True
+}
+
+response = client.drafts.create(
+ identifier=grant_id,
+ request_body=draft_request
+)
+```
+
+## Important Notes
+
+- **Backwards Compatibility**: Existing code without `is_plaintext` continues to work unchanged
+- **Default Behavior**: When `is_plaintext` is not specified, it defaults to `false` (HTML)
+- **Content Type**: The property affects MIME structure, not just content rendering
+- **Safety**: The example includes commented API calls to prevent unintended message sends
+
+## Error Handling
+
+The example includes proper error handling for:
+- Missing environment variables
+- API authentication errors
+- Invalid request parameters
+- Network connectivity issues
+
+## Documentation
+
+For more information about the Nylas Python SDK and message properties, visit:
+- [Nylas Python SDK Documentation](https://developer.nylas.com/docs/sdks/python/)
+- [Nylas API Messages Reference](https://developer.nylas.com/docs/api/v3/ecc/#tag--Messages)
+- [Nylas API Drafts Reference](https://developer.nylas.com/docs/api/v3/ecc/#tag--Drafts)
diff --git a/examples/is_plaintext_demo/is_plaintext_example.py b/examples/is_plaintext_demo/is_plaintext_example.py
new file mode 100644
index 00000000..3b3a1c23
--- /dev/null
+++ b/examples/is_plaintext_demo/is_plaintext_example.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using is_plaintext for Messages and Drafts
+
+This example demonstrates how to use the new 'is_plaintext' property when sending
+messages and creating drafts to control whether content is sent as plain text or HTML.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/is_plaintext_demo/is_plaintext_example.py
+"""
+
+import os
+import sys
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_separator(title: str) -> None:
+ """Print a formatted section separator."""
+ print(f"\n=== {title} ===")
+
+
+def demonstrate_plaintext_message(client: Client, grant_id: str) -> None:
+ """Demonstrate sending a message with is_plaintext=True."""
+ print_separator("Sending Plain Text Message")
+
+ try:
+ print("Sending message with is_plaintext=True...")
+
+ # Example message content with HTML tags that will be sent as plain text
+ body_content = """Hello World!
+
+This is a test message sent as plain text.
+Even if this contained HTML tags, they would be sent as plain text.
+
+Best regards,
+The Nylas SDK Team"""
+
+ # Send message with is_plaintext=True
+ message_request = {
+ "to": [{"email": "test@example.com", "name": "Test Recipient"}],
+ "subject": "Plain Text Message Example",
+ "body": body_content,
+ "is_plaintext": True
+ }
+
+ print("โ Message request prepared with is_plaintext=True")
+ print(f" Subject: {message_request['subject']}")
+ print(f" Body preview: {body_content[:100]}...")
+ print(f" is_plaintext: {message_request['is_plaintext']}")
+
+ # Note: Uncomment the following line to actually send the message
+ # response = client.messages.send(identifier=grant_id, request_body=message_request)
+ # print(f"โ Message sent! ID: {response.data.id}")
+
+ print("๐ง Message configured to be sent as plain text (MIME without HTML version)")
+
+ except Exception as e:
+ print(f"โ Error sending plain text message: {e}")
+
+
+def demonstrate_html_message(client: Client, grant_id: str) -> None:
+ """Demonstrate sending a message with is_plaintext=False (HTML)."""
+ print_separator("Sending HTML Message")
+
+ try:
+ print("Sending message with is_plaintext=False (HTML)...")
+
+ # Example message content with HTML formatting
+ body_content = """
+
+
+
+"""
+
+ # Send message with is_plaintext=False (default behavior)
+ message_request = {
+ "to": [{"email": "test@example.com", "name": "Test Recipient"}],
+ "subject": "HTML Message Example",
+ "body": body_content,
+ "is_plaintext": False
+ }
+
+ print("โ Message request prepared with is_plaintext=False")
+ print(f" Subject: {message_request['subject']}")
+ print(f" HTML body includes formatting tags")
+ print(f" is_plaintext: {message_request['is_plaintext']}")
+
+ # Note: Uncomment the following line to actually send the message
+ # response = client.messages.send(identifier=grant_id, request_body=message_request)
+ # print(f"โ Message sent! ID: {response.data.id}")
+
+ print("๐ Message configured to be sent as HTML (MIME includes HTML version)")
+
+ except Exception as e:
+ print(f"โ Error sending HTML message: {e}")
+
+
+def demonstrate_backwards_compatibility(client: Client, grant_id: str) -> None:
+ """Demonstrate that existing code without is_plaintext still works."""
+ print_separator("Backwards Compatibility (No is_plaintext specified)")
+
+ try:
+ print("Sending message without is_plaintext property...")
+
+ # Traditional message request without is_plaintext
+ message_request = {
+ "to": [{"email": "test@example.com", "name": "Test Recipient"}],
+ "subject": "Traditional Message Example",
+ "body": "This message doesn't specify is_plaintext, so it uses the default behavior."
+ }
+
+ print("โ Message request prepared without is_plaintext property")
+ print(f" Subject: {message_request['subject']}")
+ print(f" Body: {message_request['body']}")
+ print(f" is_plaintext: Not specified (uses API default)")
+
+ # Note: Uncomment the following line to actually send the message
+ # response = client.messages.send(identifier=grant_id, request_body=message_request)
+ # print(f"โ Message sent! ID: {response.data.id}")
+
+ print("โ Existing code continues to work without modification")
+
+ except Exception as e:
+ print(f"โ Error sending traditional message: {e}")
+
+
+def demonstrate_plaintext_draft(client: Client, grant_id: str) -> None:
+ """Demonstrate creating a draft with is_plaintext=True."""
+ print_separator("Creating Plain Text Draft")
+
+ try:
+ print("Creating draft with is_plaintext=True...")
+
+ draft_request = {
+ "to": [{"email": "test@example.com", "name": "Test Recipient"}],
+ "subject": "Plain Text Draft Example",
+ "body": "This is a draft that will be sent as plain text when sent.",
+ "is_plaintext": True
+ }
+
+ print("โ Draft request prepared with is_plaintext=True")
+ print(f" Subject: {draft_request['subject']}")
+ print(f" Body: {draft_request['body']}")
+ print(f" is_plaintext: {draft_request['is_plaintext']}")
+
+ # Note: Uncomment the following lines to actually create the draft
+ # response = client.drafts.create(identifier=grant_id, request_body=draft_request)
+ # print(f"โ Draft created! ID: {response.data.id}")
+
+ print("๐ Draft configured to be sent as plain text when sent")
+
+ except Exception as e:
+ print(f"โ Error creating plain text draft: {e}")
+
+
+def demonstrate_html_draft(client: Client, grant_id: str) -> None:
+ """Demonstrate creating a draft with is_plaintext=False."""
+ print_separator("Creating HTML Draft")
+
+ try:
+ print("Creating draft with is_plaintext=False...")
+
+ html_body = """
+
+
Draft Message
+
This is a draft with HTML formatting.
+
It will include HTML in the MIME data when sent.
+
+"""
+
+ draft_request = {
+ "to": [{"email": "test@example.com", "name": "Test Recipient"}],
+ "subject": "HTML Draft Example",
+ "body": html_body,
+ "is_plaintext": False
+ }
+
+ print("โ Draft request prepared with is_plaintext=False")
+ print(f" Subject: {draft_request['subject']}")
+ print(f" HTML body includes formatting")
+ print(f" is_plaintext: {draft_request['is_plaintext']}")
+
+ # Note: Uncomment the following lines to actually create the draft
+ # response = client.drafts.create(identifier=grant_id, request_body=draft_request)
+ # print(f"โ Draft created! ID: {response.data.id}")
+
+ print("๐ Draft configured to be sent as HTML when sent")
+
+ except Exception as e:
+ print(f"โ Error creating HTML draft: {e}")
+
+
+def demonstrate_draft_update(client: Client, grant_id: str) -> None:
+ """Demonstrate updating a draft with is_plaintext property."""
+ print_separator("Updating Draft with is_plaintext")
+
+ try:
+ print("Demonstrating draft update with is_plaintext...")
+
+ # Example update request
+ update_request = {
+ "subject": "Updated Draft with Plain Text",
+ "body": "This draft has been updated to use plain text format.",
+ "is_plaintext": True
+ }
+
+ print("โ Draft update request prepared with is_plaintext=True")
+ print(f" Updated subject: {update_request['subject']}")
+ print(f" Updated body: {update_request['body']}")
+ print(f" is_plaintext: {update_request['is_plaintext']}")
+
+ # Note: Uncomment the following lines to actually update a draft
+ # draft_id = "your_draft_id_here"
+ # response = client.drafts.update(
+ # identifier=grant_id,
+ # draft_id=draft_id,
+ # request_body=update_request
+ # )
+ # print(f"โ Draft updated! ID: {response.data.id}")
+
+ print("๐ Draft update includes is_plaintext configuration")
+
+ except Exception as e:
+ print(f"โ Error updating draft: {e}")
+
+
+def main():
+ """Main function demonstrating is_plaintext usage."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(api_key=api_key)
+
+ print("Demonstrating is_plaintext Property Usage")
+ print("=======================================")
+ print("This shows the new 'is_plaintext' property for messages and drafts")
+ print("Note: Actual API calls are commented out to prevent unintended sends")
+
+ # Demonstrate message sending with different is_plaintext values
+ demonstrate_plaintext_message(client, grant_id)
+ demonstrate_html_message(client, grant_id)
+ demonstrate_backwards_compatibility(client, grant_id)
+
+ # Demonstrate draft creation and updating with is_plaintext
+ demonstrate_plaintext_draft(client, grant_id)
+ demonstrate_html_draft(client, grant_id)
+ demonstrate_draft_update(client, grant_id)
+
+ print("\n" + "="*60)
+ print("Example completed successfully!")
+ print("="*60)
+ print("\n๐ก Key Takeaways:")
+ print("โข is_plaintext=True: Sends content as plain text (no HTML in MIME)")
+ print("โข is_plaintext=False: Sends content as HTML (includes HTML in MIME)")
+ print("โข Not specified: Uses API default behavior (backwards compatible)")
+ print("โข Available in: messages.send(), drafts.create(), drafts.update()")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/message_fields_demo/README.md b/examples/message_fields_demo/README.md
new file mode 100644
index 00000000..5b0a2f18
--- /dev/null
+++ b/examples/message_fields_demo/README.md
@@ -0,0 +1,99 @@
+# Message Fields Demo
+
+This example demonstrates the usage of the new message `fields` query parameter values (`include_tracking_options` and `raw_mime`) introduced in the Nylas API. These fields allow you to access tracking information and raw MIME data for messages.
+
+## Features Demonstrated
+
+1. **include_tracking_options Field**: Shows how to fetch messages with their tracking options (opens, thread_replies, links, and label).
+2. **raw_mime Field**: Demonstrates how to retrieve the raw MIME content of messages as Base64url-encoded data.
+3. **Backwards Compatibility**: Shows that existing code continues to work as expected without specifying the new fields.
+4. **TrackingOptions Model**: Demonstrates working with the new TrackingOptions dataclass for serialization and deserialization.
+
+## API Fields Overview
+
+### include_tracking_options
+When using `fields=include_tracking_options`, the API returns messages with their tracking settings:
+- `opens`: Boolean indicating if message open tracking is enabled
+- `thread_replies`: Boolean indicating if thread replied tracking is enabled
+- `links`: Boolean indicating if link clicked tracking is enabled
+- `label`: String label describing the message tracking purpose
+
+### raw_mime
+When using `fields=raw_mime`, the API returns only essential fields plus the raw MIME content:
+- `grant_id`: The grant identifier
+- `object`: The object type ("message")
+- `id`: The message identifier
+- `raw_mime`: Base64url-encoded string containing the complete message data
+
+## Setup
+
+1. Install the SDK in development mode from the repository root:
+```bash
+cd /path/to/nylas-python
+pip install -e .
+```
+
+2. Set your environment variables:
+```bash
+export NYLAS_API_KEY="your_api_key"
+export NYLAS_GRANT_ID="your_grant_id"
+```
+
+3. Run the example from the repository root:
+```bash
+python examples/message_fields_demo/message_fields_example.py
+```
+
+## Example Output
+
+```
+Demonstrating Message Fields Usage
+=================================
+
+=== Standard Message Fetching (Backwards Compatible) ===
+Fetching messages with standard fields...
+โ Found 2 messages with standard payload
+
+=== Include Tracking Options ===
+Fetching messages with tracking options...
+โ Found 2 messages with tracking data
+Message tracking: opens=True, links=False, label="Campaign A"
+
+=== Raw MIME Content ===
+Fetching messages with raw MIME data...
+โ Found 2 messages with raw MIME content
+Raw MIME length: 1245 characters
+
+=== TrackingOptions Model Demo ===
+Creating and serializing TrackingOptions...
+โ TrackingOptions serialization works correctly
+
+Example completed successfully!
+```
+
+## Use Cases
+
+### Tracking Options
+- **Email Campaign Analytics**: Monitor open rates, link clicks, and thread engagement
+- **Marketing Automation**: Track customer engagement with promotional emails
+- **CRM Integration**: Feed tracking data into customer relationship management systems
+
+### Raw MIME
+- **Email Archival**: Store complete email data including headers and formatting
+- **Email Migration**: Transfer emails between systems with full fidelity
+- **Security Analysis**: Examine email headers and structure for security purposes
+- **Custom Email Parsing**: Build custom email processing pipelines
+
+## Error Handling
+
+The example includes proper error handling for:
+- Missing environment variables
+- API authentication errors
+- Empty message collections
+- Invalid field parameters
+
+## Documentation
+
+For more information about the Nylas Python SDK and message fields, visit:
+- [Nylas Python SDK Documentation](https://developer.nylas.com/docs/sdks/python/)
+- [Nylas API Messages Reference](https://developer.nylas.com/docs/api/v3/ecc/#tag--Messages)
\ No newline at end of file
diff --git a/examples/message_fields_demo/message_fields_example.py b/examples/message_fields_demo/message_fields_example.py
new file mode 100644
index 00000000..5faa8d84
--- /dev/null
+++ b/examples/message_fields_demo/message_fields_example.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using Message Fields (include_tracking_options and raw_mime)
+
+This example demonstrates how to use the new 'fields' query parameter values
+'include_tracking_options' and 'raw_mime' to access message tracking data and raw MIME content.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/message_fields_demo/message_fields_example.py
+"""
+
+import os
+import sys
+import json
+import base64
+from nylas import Client
+from nylas.models.messages import TrackingOptions
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_separator(title: str) -> None:
+ """Print a formatted section separator."""
+ print(f"\n=== {title} ===")
+
+
+def demonstrate_standard_fields(client: Client, grant_id: str) -> None:
+ """Demonstrate backwards compatible message fetching (standard fields)."""
+ print_separator("Standard Message Fetching (Backwards Compatible)")
+
+ try:
+ print("Fetching messages with standard fields...")
+ messages = client.messages.list(
+ identifier=grant_id,
+ query_params={"limit": 2}
+ )
+
+ if not messages.data:
+ print("โ ๏ธ No messages found in this account")
+ return
+
+ print(f"โ Found {len(messages.data)} messages with standard payload")
+
+ for i, message in enumerate(messages.data, 1):
+ print(f"\nMessage {i}:")
+ print(f" ID: {message.id}")
+ print(f" Subject: {message.subject or 'No subject'}")
+ print(f" From: {message.from_[0].email if message.from_ else 'Unknown'}")
+ print(f" Tracking Options: {message.tracking_options}") # Should be None
+ print(f" Raw MIME: {message.raw_mime}") # Should be None
+
+ except Exception as e:
+ print(f"โ Error fetching standard messages: {e}")
+
+
+def demonstrate_tracking_options(client: Client, grant_id: str) -> None:
+ """Demonstrate fetching messages with tracking options."""
+ print_separator("Include Tracking Options")
+
+ try:
+ print("Fetching messages with tracking options...")
+ messages = client.messages.list(
+ identifier=grant_id,
+ query_params={
+ "limit": 2,
+ "fields": "include_tracking_options"
+ }
+ )
+
+ if not messages.data:
+ print("โ ๏ธ No messages found in this account")
+ return
+
+ print(f"โ Found {len(messages.data)} messages with tracking data")
+
+ for i, message in enumerate(messages.data, 1):
+ print(f"\nMessage {i}:")
+ print(f" ID: {message.id}")
+ print(f" Subject: {message.subject or 'No subject'}")
+
+ if message.tracking_options:
+ print(f" Tracking Options:")
+ print(f" Opens: {message.tracking_options.opens}")
+ print(f" Thread Replies: {message.tracking_options.thread_replies}")
+ print(f" Links: {message.tracking_options.links}")
+ print(f" Label: {message.tracking_options.label}")
+ else:
+ print(" Tracking Options: None (tracking not enabled for this message)")
+
+ except Exception as e:
+ print(f"โ Error fetching messages with tracking options: {e}")
+
+
+def demonstrate_raw_mime(client: Client, grant_id: str) -> None:
+ """Demonstrate fetching messages with raw MIME content."""
+ print_separator("Raw MIME Content")
+
+ try:
+ print("Fetching messages with raw MIME data...")
+ messages = client.messages.list(
+ identifier=grant_id,
+ query_params={
+ "limit": 2,
+ "fields": "raw_mime"
+ }
+ )
+
+ if not messages.data:
+ print("โ ๏ธ No messages found in this account")
+ return
+
+ print(f"โ Found {len(messages.data)} messages with raw MIME content")
+
+ for i, message in enumerate(messages.data, 1):
+ print(f"\nMessage {i}:")
+ print(f" ID: {message.id}")
+ print(f" Grant ID: {message.grant_id}")
+ print(f" Object: {message.object}")
+
+ if message.raw_mime:
+ print(f" Raw MIME length: {len(message.raw_mime)} characters")
+
+ # Decode a small portion to show it's real MIME data
+ try:
+ # Show first 200 characters of decoded MIME
+ decoded_sample = base64.urlsafe_b64decode(
+ message.raw_mime + '=' * (4 - len(message.raw_mime) % 4)
+ ).decode('utf-8', errors='ignore')[:200]
+ print(f" MIME preview: {decoded_sample}...")
+ except Exception as decode_error:
+ print(f" MIME preview: Unable to decode preview ({decode_error})")
+ else:
+ print(" Raw MIME: None")
+
+ # Note: In raw_mime mode, most other fields should be None
+ print(f" Subject (should be None): {message.subject}")
+ print(f" Body (should be None): {getattr(message, 'body', 'N/A')}")
+
+ except Exception as e:
+ print(f"โ Error fetching messages with raw MIME: {e}")
+
+
+def demonstrate_single_message_fields(client: Client, grant_id: str) -> None:
+ """Demonstrate fetching a single message with different field options."""
+ print_separator("Single Message with Different Fields")
+
+ try:
+ # First get a message ID
+ print("Finding a message to demonstrate single message field options...")
+ messages = client.messages.list(
+ identifier=grant_id,
+ query_params={"limit": 1}
+ )
+
+ if not messages.data:
+ print("โ ๏ธ No messages found for single message demo")
+ return
+
+ message_id = messages.data[0].id
+ print(f"Using message ID: {message_id}")
+
+ # Fetch with tracking options
+ print("\nFetching single message with tracking options...")
+ message = client.messages.find(
+ identifier=grant_id,
+ message_id=message_id,
+ query_params={"fields": "include_tracking_options"}
+ )
+
+ print(f"โ Message fetched with tracking: {message.tracking_options is not None}")
+
+ # Fetch with raw MIME
+ print("\nFetching single message with raw MIME...")
+ message = client.messages.find(
+ identifier=grant_id,
+ message_id=message_id,
+ query_params={"fields": "raw_mime"}
+ )
+
+ print(f"โ Message fetched with raw MIME: {message.raw_mime is not None}")
+ if message.raw_mime:
+ print(f" Raw MIME size: {len(message.raw_mime)} characters")
+
+ except Exception as e:
+ print(f"โ Error in single message demo: {e}")
+
+
+def demonstrate_tracking_options_model() -> None:
+ """Demonstrate working with the TrackingOptions model directly."""
+ print_separator("TrackingOptions Model Demo")
+
+ try:
+ print("Creating TrackingOptions object...")
+
+ # Create a TrackingOptions instance
+ tracking = TrackingOptions(
+ opens=True,
+ thread_replies=False,
+ links=True,
+ label="Marketing Campaign Demo"
+ )
+
+ print("โ TrackingOptions created:")
+ print(f" Opens: {tracking.opens}")
+ print(f" Thread Replies: {tracking.thread_replies}")
+ print(f" Links: {tracking.links}")
+ print(f" Label: {tracking.label}")
+
+ # Demonstrate serialization
+ print("\nSerializing to dict...")
+ tracking_dict = tracking.to_dict()
+ print(f"โ Serialized: {json.dumps(tracking_dict, indent=2)}")
+
+ # Demonstrate deserialization
+ print("\nDeserializing from dict...")
+ restored_tracking = TrackingOptions.from_dict(tracking_dict)
+ print(f"โ Deserialized: opens={restored_tracking.opens}, label='{restored_tracking.label}'")
+
+ # Demonstrate JSON serialization
+ print("\nJSON serialization...")
+ tracking_json = tracking.to_json()
+ print(f"โ JSON: {tracking_json}")
+
+ restored_from_json = TrackingOptions.from_json(tracking_json)
+ print(f"โ From JSON: {restored_from_json.to_dict()}")
+
+ except Exception as e:
+ print(f"โ Error in TrackingOptions demo: {e}")
+
+
+def main():
+ """Main function demonstrating message fields usage."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(api_key=api_key)
+
+ print("Demonstrating Message Fields Usage")
+ print("=================================")
+ print("This shows the new 'include_tracking_options' and 'raw_mime' field options")
+
+ # Demonstrate different field options
+ demonstrate_standard_fields(client, grant_id)
+ demonstrate_tracking_options(client, grant_id)
+ demonstrate_raw_mime(client, grant_id)
+ demonstrate_single_message_fields(client, grant_id)
+ demonstrate_tracking_options_model()
+
+ print("\n" + "="*50)
+ print("Example completed successfully!")
+ print("="*50)
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/metadata_field_demo/README.md b/examples/metadata_field_demo/README.md
new file mode 100644
index 00000000..f5310f31
--- /dev/null
+++ b/examples/metadata_field_demo/README.md
@@ -0,0 +1,67 @@
+# Metadata Field Example
+
+This example demonstrates how to use metadata fields when creating drafts and sending messages using the Nylas Python SDK.
+
+## Features
+
+- Create drafts with custom metadata fields
+- Send messages with custom metadata fields
+- Error handling and environment variable configuration
+- Clear output and status messages
+
+## Prerequisites
+
+1. A Nylas account with API access
+2. Python 3.x installed
+3. Local installation of the Nylas Python SDK (this repository)
+
+## Setup
+
+1. Install the SDK in development mode from the repository root:
+```bash
+cd /path/to/nylas-python
+pip install -e .
+```
+
+2. Set your environment variables:
+```bash
+export NYLAS_API_KEY="your_api_key"
+export NYLAS_GRANT_ID="your_grant_id"
+export TEST_EMAIL="recipient@example.com" # Optional
+```
+
+3. Run the example from the repository root:
+```bash
+python examples/metadata_field_demo/metadata_example.py
+```
+
+## Example Output
+
+```
+Demonstrating Metadata Field Usage
+=================================
+
+1. Creating draft with metadata...
+โ Created draft with ID: draft-abc123
+ Request ID: req-xyz789
+
+2. Sending message with metadata...
+โ Sent message with ID: msg-def456
+ Request ID: req-uvw321
+
+Example completed successfully!
+```
+
+## Error Handling
+
+The example includes proper error handling for:
+- Missing environment variables
+- API authentication errors
+- Draft creation failures
+- Message sending failures
+
+## Documentation
+
+For more information about the Nylas Python SDK and its features, visit:
+- [Nylas Python SDK Documentation](https://developer.nylas.com/docs/sdks/python/)
+- [Nylas API Reference](https://developer.nylas.com/docs/api/)
diff --git a/examples/metadata_field_demo/metadata_example.py b/examples/metadata_field_demo/metadata_example.py
new file mode 100644
index 00000000..737dbaf7
--- /dev/null
+++ b/examples/metadata_field_demo/metadata_example.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using Metadata Fields with Drafts and Messages
+
+This example demonstrates how to use metadata fields when creating drafts
+and sending messages using the Nylas Python SDK.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+ TEST_EMAIL: Email address for sending test messages (optional)
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ export TEST_EMAIL="recipient@example.com"
+ python examples/metadata_field_demo/metadata_example.py
+"""
+
+import os
+import sys
+from typing import Dict, Any, Optional
+
+# Import from local nylas package
+from nylas import Client
+from nylas.models.errors import NylasApiError
+
+
+def get_env_or_exit(var_name: str, required: bool = True) -> Optional[str]:
+ """Get an environment variable or exit if required and not found."""
+ value = os.getenv(var_name)
+ if required and not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def create_draft_with_metadata(
+ client: Client, grant_id: str, metadata: Dict[str, Any], recipient: str
+) -> str:
+ """Create a draft message with metadata fields."""
+ try:
+ draft_request = {
+ "subject": "Test Draft with Metadata",
+ "to": [{"email": recipient}],
+ "body": "This is a test draft with metadata fields.",
+ "metadata": metadata
+ }
+
+ draft, request_id = client.drafts.create(
+ identifier=grant_id,
+ request_body=draft_request
+ )
+ print(f"โ Created draft with ID: {draft.id}")
+ print(f" Request ID: {request_id}")
+ return draft.id
+ except NylasApiError as e:
+ print(f"โ Failed to create draft: {e}")
+ sys.exit(1)
+
+
+def send_message_with_metadata(
+ client: Client, grant_id: str, metadata: Dict[str, Any], recipient: str
+) -> str:
+ """Send a message directly with metadata fields."""
+ try:
+ message_request = {
+ "subject": "Test Message with Metadata",
+ "to": [{"email": recipient}],
+ "body": "This is a test message with metadata fields.",
+ "metadata": metadata
+ }
+
+ message, request_id = client.messages.send(
+ identifier=grant_id,
+ request_body=message_request
+ )
+ print(f"โ Sent message with ID: {message.id}")
+ print(f" Request ID: {request_id}")
+
+ return message.id
+ except NylasApiError as e:
+ print(f"โ Failed to send message: {e}")
+ sys.exit(1)
+
+
+def main():
+ """Main function demonstrating metadata field usage."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+ recipient = get_env_or_exit("TEST_EMAIL", required=False) or "recipient@example.com"
+
+ # Initialize Nylas client
+ client = Client(
+ api_key=api_key,
+ )
+
+ # Example metadata
+ metadata = {
+ "campaign_id": "example-123",
+ "user_id": "user-456",
+ "custom_field": "test-value"
+ }
+
+ print("\nDemonstrating Metadata Field Usage")
+ print("=================================")
+
+ # Create a draft with metadata
+ print("\n1. Creating draft with metadata...")
+ draft_id = create_draft_with_metadata(client, grant_id, metadata, recipient)
+
+ # Send a message with metadata
+ print("\n2. Sending message with metadata...")
+ message_id = send_message_with_metadata(client, grant_id, metadata, recipient)
+
+ print("\nExample completed successfully!")
+
+ # Get the draft and message to demonstrate metadata retrieval
+ draft = client.drafts.find(identifier=grant_id, draft_id=draft_id)
+ message = client.messages.find(identifier=grant_id, message_id=message_id)
+
+ print("\nRetrieved Draft Metadata:")
+ print("-------------------------")
+ print(draft.data)
+
+ print("\nRetrieved Message Metadata:")
+ print("---------------------------")
+ print(message.data)
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/native-authentication-exchange/README.md b/examples/native-authentication-exchange/README.md
deleted file mode 100644
index 09ef1d0d..00000000
--- a/examples/native-authentication-exchange/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Example: Native Authentication (Exchange)
-
-This is an example project that demonstrates how to connect to Nylas using the
-[Native Authentication](https://docs.nylas.com/reference#native-authentication-1)
-flow. Note that different email providers have different native authentication
-processes; this example project *only* works with Microsoft Exchange.
-
-This example uses the [Flask](http://flask.pocoo.org/) web framework to make
-a small website, and uses the [Flask-WTF](https://flask-wtf.readthedocs.io/)
-extension to implement an HTML form so the user can type in their
-Exchange account information.
-Once the native authentication has been set up, this example website will contact
-the Nylas API to learn some basic information about the current user,
-such as the user's name and email address. It will display that information
-on the page, just to prove that it can fetch it correctly.
-
-In order to successfully run this example, you need to do the following things:
-
-## Get a client ID & client secret from Nylas
-
-To do this, make a [Nylas Developer](https://developer.nylas.com/) account.
-You should see your client ID and client secret on the dashboard,
-once you've logged in on the
-[Nylas Developer](https://developer.nylas.com/) website.
-
-## Update the `config.json` File
-
-Open the `config.json` file in this directory, and replace the example
-values with the real values. This is where you'll need the client ID and
-client secret fron Nylas. You'll also need to replace the example secret key with
-any random string of letters and numbers: a keyboard mash will do.
-
-## Install the Dependencies
-
-This project depends on a few third-party Python modules, like Flask.
-These dependencies are listed in the `requirements.txt` file in this directory.
-To install them, use the `pip` tool, like this:
-
-```
-pip install -r requirements.txt
-```
-
-## Run the Example
-
-Finally, run the example project like this:
-
-```
-python server.py
-```
-
-Once the server is running, visit `http://127.0.0.1:5000/` in your browser
-to test it out!
diff --git a/examples/native-authentication-exchange/config.json b/examples/native-authentication-exchange/config.json
deleted file mode 100644
index 9b54e3df..00000000
--- a/examples/native-authentication-exchange/config.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "SECRET_KEY": "replace me with a random string",
- "NYLAS_OAUTH_CLIENT_ID": "replace me with the client ID from Nylas",
- "NYLAS_OAUTH_CLIENT_SECRET": "replace me with the client secret from Nylas"
-}
diff --git a/examples/native-authentication-exchange/requirements.txt b/examples/native-authentication-exchange/requirements.txt
deleted file mode 100644
index f03e8299..00000000
--- a/examples/native-authentication-exchange/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Flask>=0.11
-Flask-WTF
-requests
diff --git a/examples/native-authentication-exchange/server.py b/examples/native-authentication-exchange/server.py
deleted file mode 100644
index 3fefd926..00000000
--- a/examples/native-authentication-exchange/server.py
+++ /dev/null
@@ -1,170 +0,0 @@
-# Imports from the Python standard library
-from __future__ import print_function
-import os
-import sys
-import textwrap
-
-# Imports from third-party modules that this project depends on
-try:
- import requests
- from flask import Flask, render_template, session, redirect, url_for
- from flask_wtf import FlaskForm
- from wtforms.fields import StringField, PasswordField
- from wtforms.fields.html5 import EmailField
- from wtforms.validators import DataRequired
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the dependencies for this project.
- To do so, run this command:
-
- pip install -r requirements.txt
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-try:
- from nylas import APIClient
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the Nylas SDK for this project.
- To do so, run this command:
-
- pip install nylas
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# This example uses Flask, a micro web framework written in Python.
-# For more information, check out the documentation: http://flask.pocoo.org
-# Create a Flask app, and load the configuration file.
-app = Flask(__name__)
-app.config.from_json("config.json")
-
-# Check for dummy configuration values.
-# If you are building your own application based on this example,
-# you can remove this check from your code.
-cfg_needs_replacing = [
- key
- for key, value in app.config.items()
- if isinstance(value, str) and value.startswith("replace me")
-]
-if cfg_needs_replacing:
- message = textwrap.dedent(
- """
- This example will only work if you replace the fake configuration
- values in `config.json` with real configuration values.
- The following config values need to be replaced:
- {keys}
- Consult the README.md file in this directory for more information.
- """
- ).format(keys=", ".join(cfg_needs_replacing))
- print(message, file=sys.stderr)
- sys.exit(1)
-
-
-class ExchangeCredentialsForm(FlaskForm):
- name = StringField("Name", validators=[DataRequired()])
- email = EmailField("Email Address", validators=[DataRequired()])
- password = PasswordField("Password", validators=[DataRequired()])
- server_host = StringField("Server Host", render_kw={"placeholder": "(optional)"})
-
-
-class APIError(Exception):
- pass
-
-
-# Define what Flask should do when someone visits the root URL of this website.
-@app.route("/", methods=("GET", "POST"))
-def index():
- form = ExchangeCredentialsForm()
- api_error = None
- if form.validate_on_submit():
- try:
- return pass_creds_to_nylas(
- name=form.name.data,
- email=form.email.data,
- password=form.password.data,
- server_host=form.server_host.data,
- )
- except APIError as err:
- api_error = err.args[0]
- return render_template("index.html", form=form, api_error=api_error)
-
-
-def pass_creds_to_nylas(name, email, password, server_host=None):
- """
- Passes Exchange credentials to Nylas, to set up native authentication.
- """
- # Start the connection process by looking up all the information that
- # Nylas needs in order to connect, and sending it to the authorize API.
- nylas_authorize_data = {
- "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"],
- "name": name,
- "email_address": email,
- "provider": "exchange",
- "settings": {"username": email, "password": password},
- }
- if server_host:
- nylas_authorize_data["settings"]["eas_server_host"] = server_host
-
- nylas_authorize_resp = requests.post(
- "https://api.nylas.com/connect/authorize", json=nylas_authorize_data
- )
- if not nylas_authorize_resp.ok:
- message = nylas_authorize_resp.json()["message"]
- raise APIError(message)
-
- nylas_code = nylas_authorize_resp.json()["code"]
-
- # Now that we've got the `code` from the authorize response,
- # pass it to the token response to complete the connection.
- nylas_token_data = {
- "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"],
- "client_secret": app.config["NYLAS_OAUTH_CLIENT_SECRET"],
- "code": nylas_code,
- }
- nylas_token_resp = requests.post(
- "https://api.nylas.com/connect/token", json=nylas_token_data
- )
- if not nylas_token_resp.ok:
- message = nylas_token_resp.json()["message"]
- raise APIError(message)
-
- nylas_access_token = nylas_token_resp.json()["access_token"]
-
- # Great, we've connected the Exchange account to Nylas!
- # In the process, Nylas gave us an OAuth access token, which we'll need
- # in order to make API requests to Nylas in the future.
- # We'll save that access token in the Flask session, so we can pick it up
- # later and use it when we need it.
- session["nylas_access_token"] = nylas_access_token
-
- # We're all done here. Redirect the user back to the success page,
- # which will pick up the access token we just saved.
- return redirect(url_for("success"))
-
-
-@app.route("/success")
-def success():
- if "nylas_access_token" not in session:
- return render_template("missing_token.html")
-
- client = APIClient(
- client_id=app.config["NYLAS_OAUTH_CLIENT_ID"],
- client_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"],
- access_token=session["nylas_access_token"],
- )
-
- # We'll use the Nylas client to fetch information from Nylas
- # about the current user, and pass that to the template.
- account = client.account
- return render_template("success.html", account=account)
-
-
-# When this file is executed, run the Flask web server.
-if __name__ == "__main__":
- app.run()
diff --git a/examples/native-authentication-exchange/templates/base.html b/examples/native-authentication-exchange/templates/base.html
deleted file mode 100644
index b0683cbb..00000000
--- a/examples/native-authentication-exchange/templates/base.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- Nylas Native Authentication: Exchange
-
-
-
-
You've successfully connected to Nylas via native authentication!
- Here's some information that I got from the Nylas API, to prove it:
-
-
- {% for key, value in account.items() %}
-
-
{{ key }}
-
{{ value }}
-
- {% endfor %}
-
-
-{% endblock %}
diff --git a/examples/native-authentication-gmail/README.md b/examples/native-authentication-gmail/README.md
deleted file mode 100644
index 2de2cbff..00000000
--- a/examples/native-authentication-gmail/README.md
+++ /dev/null
@@ -1,108 +0,0 @@
-# Example: Native Authentication (Gmail)
-
-This is an example project that demonstrates how to connect to Nylas using the
-[Native Authentication](https://docs.nylas.com/reference#native-authentication-1)
-flow. Note that different email providers have different native authentication
-processes; this example project *only* works with Gmail.
-
-This example uses the [Flask](http://flask.pocoo.org/) web framework to make
-a small website, and uses the [Flask-Dance](http://flask-dance.rtfd.org/)
-extension to handle the tricky bits of implementing the
-[OAuth protocol](https://oauth.net/).
-Once the native authentication has been set up, this example website will contact
-the Nylas API to learn some basic information about the current user,
-such as the user's name and email address. It will display that information
-on the page, just to prove that it can fetch it correctly.
-
-In order to successfully run this example, you need to do the following things:
-
-## Get a client ID & client secret from Nylas
-
-To do this, make a [Nylas Developer](https://developer.nylas.com/) account.
-You should see your client ID and client secret on the dashboard,
-once you've logged in on the
-[Nylas Developer](https://developer.nylas.com/) website.
-
-## Get a client ID & client secret from Google
-
-To do this, go to the
-[Google Developers Console](https://console.developers.google.com)
-and create a project. Then go to the "Library" section and enable the
-following APIs: "Gmail API", "Contacts API", "Google Calendar API".
-Then go to the "Credentials" section and create a new OAuth client ID.
-Select "Web application" for the application type, and click the "Create"
-button.
-
-Check out the
-[Google OAuth Setup Guide](https://support.nylas.com/hc/en-us/articles/222176307)
-on the Nylas support website, for more information.
-
-## Update the `config.json` File
-
-Open the `config.json` file in this directory, and replace the example
-values with the real values. You'll need the client ID and client secret
-from Nylas, and the client ID and client secret from Google.
-
-You'll also need to replace the example secret key with
-any random string of letters and numbers: a keyboard mash will do.
-
-## Set Up HTTPS
-
-The OAuth protocol requires that all communication occur via the secure HTTPS
-connections, rather than insecure HTTP connections. There are several ways
-to set up HTTPS on your computer, but perhaps the simplest is to use
-[ngrok](https://ngrok.com), a tool that lets you create a secure tunnel
-from the ngrok website to your computer. Install it from the website, and
-then run the following command:
-
-```
-ngrok http 5000
-```
-
-Notice that ngrok will show you two "forwarding" URLs, which may look something
-like `http://ed90abe7.ngrok.io` and `https://ed90abe7.ngrok.io`. (The hash
-subdomain will be different for you.) You'll be using the second URL, which
-starts with `https`.
-
-Alternatively, you can set the `OAUTHLIB_INSECURE_TRANSPORT` environment
-variable in your shell, to disable the HTTPS check. That way, you'll be
-able to use `localhost` to refer to your app, instead of an ngrok URL.
-However, be aware that you won't be able to do this when you deploy
-your app to production, so it's usually a better idea to set up HTTPS properly.
-
-## Set the Authorized Redirect URI for Google
-
-Once you have a HTTPS URL that points to your computer, you'll need to tell
-Google about it. On the
-[Google Developer Console](https://console.developers.google.com),
-click on the "Credentials" section, find the OAuth client that you
-already created, and click on the "edit" button on the right side.
-There is a section called "Authorized redirect URIs"; this is where
-you need to tell Google about your HTTPS URL.
-Paste your HTTPS URL into text field, and add `/login/google/authorized`
-after it. For example, if your HTTPS URL is `https://ad172180.ngrok.io`, then
-you would put `https://ad172180.ngrok.io/login/google/authorized` into
-the "Authorized redirect URIs" text field.
-
-Then click the "Done" button to save. Even after you save, it usually takes
-Google about 5 minutes to update everything behind the scenes.
-
-## Install the Dependencies
-
-This project depends on a few third-party Python modules, like Flask.
-These dependencies are listed in the `requirements.txt` file in this directory.
-To install them, use the `pip` tool, like this:
-
-```
-pip install -r requirements.txt
-```
-
-## Run the Example
-
-Finally, run the example project like this:
-
-```
-python server.py
-```
-
-Once the server is running, visit the ngrok URL in your browser to test it out!
diff --git a/examples/native-authentication-gmail/config.json b/examples/native-authentication-gmail/config.json
deleted file mode 100644
index f997e0fe..00000000
--- a/examples/native-authentication-gmail/config.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "SECRET_KEY": "replace me with a random string",
- "NYLAS_OAUTH_CLIENT_ID": "replace me with the client ID from Nylas",
- "NYLAS_OAUTH_CLIENT_SECRET": "replace me with the client secret from Nylas",
- "GOOGLE_OAUTH_CLIENT_ID": "replace me with the client ID from Google",
- "GOOGLE_OAUTH_CLIENT_SECRET": "replace me with the client secret from Google"
-}
diff --git a/examples/native-authentication-gmail/requirements.txt b/examples/native-authentication-gmail/requirements.txt
deleted file mode 100644
index e87c213b..00000000
--- a/examples/native-authentication-gmail/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Flask>=0.11
-Flask-Dance>=0.11.1
-requests
diff --git a/examples/native-authentication-gmail/server.py b/examples/native-authentication-gmail/server.py
deleted file mode 100644
index d79e6593..00000000
--- a/examples/native-authentication-gmail/server.py
+++ /dev/null
@@ -1,232 +0,0 @@
-# Imports from the Python standard library
-from __future__ import print_function
-import os
-import sys
-import textwrap
-
-# Imports from third-party modules that this project depends on
-try:
- import requests
- from flask import Flask, render_template, session, redirect, url_for
- from werkzeug.middleware.proxy_fix import ProxyFix
- from flask_dance.contrib.google import make_google_blueprint, google
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the dependencies for this project.
- To do so, run this command:
-
- pip install -r requirements.txt
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-try:
- from nylas import APIClient
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the Nylas SDK for this project.
- To do so, run this command:
-
- pip install nylas
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# This example uses Flask, a micro web framework written in Python.
-# For more information, check out the documentation: http://flask.pocoo.org
-# Create a Flask app, and load the configuration file.
-app = Flask(__name__)
-app.config.from_json("config.json")
-
-# Check for dummy configuration values.
-# If you are building your own application based on this example,
-# you can remove this check from your code.
-cfg_needs_replacing = [
- key
- for key, value in app.config.items()
- if isinstance(value, str) and value.startswith("replace me")
-]
-if cfg_needs_replacing:
- message = textwrap.dedent(
- """
- This example will only work if you replace the fake configuration
- values in `config.json` with real configuration values.
- The following config values need to be replaced:
- {keys}
- Consult the README.md file in this directory for more information.
- """
- ).format(keys=", ".join(cfg_needs_replacing))
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# Use Flask-Dance to automatically set up the OAuth endpoints for Google.
-# For more information, check out the documentation: http://flask-dance.rtfd.org
-google_bp = make_google_blueprint(
- scope=[
- "openid",
- "https://www.googleapis.com/auth/userinfo.email",
- "https://www.googleapis.com/auth/userinfo.profile",
- "https://mail.google.com/",
- "https://www.googleapis.com/auth/calendar",
- "https://www.googleapis.com/auth/contacts",
- ],
- offline=True, # this allows you to get a refresh token from Google
- redirect_to="after_google",
- # If you get a "missing Google refresh token" error, uncomment this line:
- # reprompt_consent=True,
- # That `reprompt_consent` argument will force Google to re-ask the user
- # every single time if they want to connect with your application.
- # Google will only send the refresh token if the user has explicitly
- # given consent.
-)
-app.register_blueprint(google_bp, url_prefix="/login")
-
-# Teach Flask how to find out that it's behind an ngrok proxy
-app.wsgi_app = ProxyFix(app.wsgi_app)
-
-# Define what Flask should do when someone visits the root URL of this website.
-@app.route("/")
-def index():
- # If the user has already connected to Google via OAuth,
- # `google.authorized` will be True. We also need to be sure that
- # we have a refresh token from Google. If we don't have both of those,
- # that indicates that we haven't correctly connected with Google.
- if not (google.authorized and "refresh_token" in google.token):
- # Google requires HTTPS. The template will display a handy warning,
- # unless we've overridden the check.
- return render_template(
- "before_google.html",
- insecure_override=os.environ.get("OAUTHLIB_INSECURE_TRANSPORT"),
- )
-
- if "nylas_access_token" not in session:
- # The user has already connected to Google via OAuth,
- # but hasn't yet passed those credentials to Nylas.
- # We'll redirect the user to the right place to make that happen.
- return redirect(url_for("after_google"))
-
- # If we've gotten to this point, then the user has already connected
- # to both Google and Nylas.
- # Let's set up the SDK client with the OAuth token:
- client = APIClient(
- client_id=app.config["NYLAS_OAUTH_CLIENT_ID"],
- client_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"],
- access_token=session["nylas_access_token"],
- )
-
- # We'll use the Nylas client to fetch information from Nylas
- # about the current user, and pass that to the template.
- account = client.account
- return render_template("after_connected.html", account=account, client=client)
-
-
-@app.route("/google/success")
-def after_google():
- """
- This just renders a confirmation page, to let the user know that
- they've successfully connected to Google and need to move on to the
- next step: passing those authentication credentials to Nylas.
- """
- return render_template("after_google.html")
-
-
-@app.route("/nylas/connect")
-def pass_creds_to_nylas():
- """
- This view loads the credentials from Google and passes them to Nylas,
- to set up native authentication.
- """
- # If you haven't already connected with Google, this won't work.
- if not google.authorized:
- return "Error: not yet connected with Google!", 400
-
- if "refresh_token" not in google.token:
- # We're missing the refresh token from Google, and the only way to get
- # a new one is to force reauthentication. That's annoying.
- return (
- (
- "Error: missing Google refresh token. "
- "Uncomment the `reprompt_consent` line in the code to fix this."
- ),
- 500,
- )
-
- # Look up the user's name and email address from Google.
- google_resp = google.get("/oauth2/v2/userinfo?fields=name,email")
- assert google_resp.ok, "Received failure response from Google userinfo API"
- google_userinfo = google_resp.json()
-
- # Start the connection process by looking up all the information that
- # Nylas needs in order to connect, and sending it to the authorize API.
- nylas_authorize_data = {
- "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"],
- "name": google_userinfo["name"],
- "email_address": google_userinfo["email"],
- "provider": "gmail",
- "settings": {
- "google_client_id": app.config["GOOGLE_OAUTH_CLIENT_ID"],
- "google_client_secret": app.config["GOOGLE_OAUTH_CLIENT_SECRET"],
- "google_refresh_token": google.token["refresh_token"],
- },
- }
- nylas_authorize_resp = requests.post(
- "https://api.nylas.com/connect/authorize", json=nylas_authorize_data
- )
- assert nylas_authorize_resp.ok, "Received failure response from Nylas authorize API"
- nylas_code = nylas_authorize_resp.json()["code"]
-
- # Now that we've got the `code` from the authorize response,
- # pass it to the token response to complete the connection.
- nylas_token_data = {
- "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"],
- "client_secret": app.config["NYLAS_OAUTH_CLIENT_SECRET"],
- "code": nylas_code,
- }
- nylas_token_resp = requests.post(
- "https://api.nylas.com/connect/token", json=nylas_token_data
- )
- assert nylas_token_resp.ok, "Received failure response from Nylas token API"
- nylas_access_token = nylas_token_resp.json()["access_token"]
-
- # Great, we've connected Google to Nylas! In the process, Nylas gave us
- # an OAuth access token, which we'll need in order to make API requests
- # to Nylas in the future. We'll save that access token in the Flask session,
- # so we can pick it up later and use it when we need it.
- session["nylas_access_token"] = nylas_access_token
-
- # We're all done here. Redirect the user back to the home page,
- # which will pick up the access token we just saved.
- return redirect(url_for("index"))
-
-
-def ngrok_url():
- """
- If ngrok is running, it exposes an API on port 4040. We can use that
- to figure out what URL it has assigned, and suggest that to the user.
- https://ngrok.com/docs#list-tunnels
- """
- try:
- ngrok_resp = requests.get("http://localhost:4040/api/tunnels")
- except requests.ConnectionError:
- # I guess ngrok isn't running.
- return None
- ngrok_data = ngrok_resp.json()
- secure_urls = [
- tunnel["public_url"]
- for tunnel in ngrok_data["tunnels"]
- if tunnel["proto"] == "https"
- ]
- return secure_urls[0]
-
-
-# When this file is executed, run the Flask web server.
-if __name__ == "__main__":
- url = ngrok_url()
- if url:
- print(" * Visit {url} to view this Nylas example".format(url=url))
-
- app.run()
diff --git a/examples/native-authentication-gmail/templates/after_connected.html b/examples/native-authentication-gmail/templates/after_connected.html
deleted file mode 100644
index 23efec8e..00000000
--- a/examples/native-authentication-gmail/templates/after_connected.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-
Done!
-
You've successfully connected to Nylas via native authentication!
- Here's some information that I got from the Nylas API, to prove it:
Great, you've succesfully connected with Google! The next step is to
- hand those authentication credentials over to Nylas, so that Nylas
- can connect with Google on your behalf.
- Please click this link to do that.
-
-
diff --git a/examples/native-authentication-gmail/templates/before_google.html b/examples/native-authentication-gmail/templates/before_google.html
deleted file mode 100644
index 07a59634..00000000
--- a/examples/native-authentication-gmail/templates/before_google.html
+++ /dev/null
@@ -1,34 +0,0 @@
-{% extends "base.html" %}
-{% block body %}
-{% if not insecure_override %}
-
-
Warning
-
OAuth requires HTTPS, and this page is loaded via insecure HTTP.
- If you try to connect to Google like this, it will fail.
- To fix this problem, serve this website over HTTPS.
-
-
Since this is just an example, you can disable this requirement by
- setting the OAUTHLIB_INSECURE_TRANSPORT environment
- variable to 1 before running this example again.
- However, you will not be able to do this when running
- in production.
-
-
-{% endif %}
-
-
Step 1: Connect with Google
-
-
Thanks for giving Nylas a try! Native Authentication is a two-step process,
- where the first step is connecting with the native email provider.
- This example is set up to work with Google, so it will only work if you
- have a Gmail account.
-
First, please click this button to connect with Google.
-Connect with Google
-
-
-{% endblock %}
diff --git a/examples/notetaker_api_demo/README.md b/examples/notetaker_api_demo/README.md
new file mode 100644
index 00000000..81b5c26c
--- /dev/null
+++ b/examples/notetaker_api_demo/README.md
@@ -0,0 +1,52 @@
+# Notetaker API Demo
+
+This demo showcases how to use the Nylas Notetaker API to create, manage, and interact with notes.
+
+## Features Demonstrated
+
+- Creating new notes
+- Retrieving notes
+- Updating notes
+- Deleting notes
+- Managing note metadata
+- Sharing notes with other users
+
+## Prerequisites
+
+- Python 3.8+
+- Nylas Python SDK (local version from this repository)
+- Nylas API credentials (Client ID and Client Secret)
+
+## Setup
+
+1. Install the SDK in development mode:
+```bash
+# From the root of the nylas-python repository
+pip install -e .
+```
+
+2. Set up your environment variables:
+```bash
+export NYLAS_API_KEY='your_api_key'
+export NYLAS_API_URI='https://api.nylas.com' # Optional, defaults to https://api.nylas.com
+```
+
+## Running the Demo
+
+From the root of the repository:
+```bash
+python examples/notetaker_api_demo/notetaker_demo.py
+```
+
+## Code Examples
+
+The demo includes examples of:
+
+1. Creating a new note
+2. Retrieving a list of notes
+3. Updating an existing note
+4. Deleting a note
+5. Managing note metadata
+6. Sharing notes with other users
+
+Each example is documented with comments explaining the functionality and expected output.
\ No newline at end of file
diff --git a/examples/notetaker_api_demo/notetaker_demo.py b/examples/notetaker_api_demo/notetaker_demo.py
new file mode 100644
index 00000000..e48d8e8a
--- /dev/null
+++ b/examples/notetaker_api_demo/notetaker_demo.py
@@ -0,0 +1,155 @@
+import os
+import sys
+import json
+from nylas import Client
+from nylas.models.notetakers import NotetakerMeetingSettingsRequest, NotetakerState, InviteNotetakerRequest
+from nylas.models.errors import NylasApiError
+
+# Initialize the Nylas client
+nylas = Client(
+ api_key=os.getenv("NYLAS_API_KEY"),
+ api_uri=os.getenv("NYLAS_API_URI", "https://api.us.nylas.com")
+)
+
+def invite_notetaker():
+ """Demonstrates how to invite a Notetaker to a meeting."""
+ print("\n=== Inviting Notetaker to Meeting ===")
+
+ try:
+ meeting_link = os.getenv("MEETING_LINK")
+ if not meeting_link:
+ raise ValueError("MEETING_LINK environment variable is not set. Please set it with your meeting URL.")
+
+ request_body: InviteNotetakerRequest = {
+ "meeting_link": meeting_link,
+ "name": "Nylas Notetaker",
+ "meeting_settings": {
+ "video_recording": True,
+ "audio_recording": True,
+ "transcription": True
+ }
+ }
+
+ print(f"Request body: {json.dumps(request_body, indent=2)}")
+
+ notetaker = nylas.notetakers.invite(request_body=request_body)
+
+ print(f"Invited Notetaker with ID: {notetaker.data.id}")
+ print(f"Name: {notetaker.data.name}")
+ print(f"State: {notetaker.data.state}")
+ return notetaker
+ except NylasApiError as e:
+ print(f"Error inviting notetaker: {str(e)}")
+ print(f"Error details: {e.__dict__}")
+ raise
+ except json.JSONDecodeError as e:
+ print(f"JSON decode error: {str(e)}")
+ raise
+ except Exception as e:
+ print(f"Unexpected error in invite_notetaker: {str(e)}")
+ print(f"Error type: {type(e)}")
+ print(f"Error details: {e.__dict__}")
+ raise
+
+def list_notetakers():
+ """Demonstrates how to list all Notetakers."""
+ print("\n=== Listing All Notetakers ===")
+
+ try:
+ notetakers = nylas.notetakers.list()
+
+ print(f"Found {len(notetakers.data)} notetakers:")
+ for notetaker in notetakers.data:
+ print(f"- {notetaker.name} (ID: {notetaker.id}, State: {notetaker.state})")
+
+ return notetakers
+ except NylasApiError as e:
+ print(f"Error listing notetakers: {str(e)}")
+ raise
+ except Exception as e:
+ print(f"Unexpected error in list_notetakers: {str(e)}")
+ raise
+
+def get_notetaker_media(notetaker_id):
+ """Demonstrates how to get media from a Notetaker."""
+ print("\n=== Getting Notetaker Media ===")
+
+ try:
+ media = nylas.notetakers.get_media(notetaker_id)
+
+ if media.recording:
+ print(f"Recording URL: {media.data.recording.url}")
+ print(f"Recording Name: {media.data.recording.name}")
+ print(f"Recording Type: {media.data.recording.type}")
+ print(f"Recording Size: {media.data.recording.size} bytes")
+ print(f"Recording Created At: {media.data.recording.created_at}")
+ print(f"Recording Expires At: {media.data.recording.expires_at}")
+ print(f"Recording TTL: {media.data.recording.ttl} seconds")
+ if media.transcript:
+ print(f"Transcript URL: {media.data.transcript.url}")
+ print(f"Transcript Name: {media.data.transcript.name}")
+ print(f"Transcript Type: {media.data.transcript.type}")
+ print(f"Transcript Size: {media.data.transcript.size} bytes")
+ print(f"Transcript Created At: {media.data.transcript.created_at}")
+ print(f"Transcript Expires At: {media.data.transcript.expires_at}")
+ print(f"Transcript TTL: {media.data.transcript.ttl} seconds")
+
+ return media
+ except NylasApiError as e:
+ print(f"Error getting notetaker media: {str(e)}")
+ raise
+ except Exception as e:
+ print(f"Unexpected error in get_notetaker_media: {str(e)}")
+ raise
+
+def leave_notetaker(notetaker_id):
+ """Demonstrates how to leave a Notetaker."""
+ print("\n=== Leaving Notetaker ===")
+
+ try:
+ nylas.notetakers.leave(notetaker_id)
+ print(f"Left Notetaker with ID: {notetaker_id}")
+ except NylasApiError as e:
+ print(f"Error leaving notetaker: {str(e)}")
+ raise
+ except Exception as e:
+ print(f"Unexpected error in leave_notetaker: {str(e)}")
+ raise
+
+def main():
+ """Main function to run all demo examples."""
+ try:
+ # Check for required environment variables
+ api_key = os.getenv("NYLAS_API_KEY")
+ if not api_key:
+ raise ValueError("NYLAS_API_KEY environment variable is not set")
+ print(f"Using API key: {api_key[:5]}...")
+
+ # Invite a Notetaker to a meeting
+ notetaker = invite_notetaker()
+
+ # List all Notetakers
+ list_notetakers()
+
+ # Get media from the Notetaker (if available)
+ if notetaker.data.state == NotetakerState.MEDIA_AVAILABLE:
+ get_notetaker_media(notetaker.data.id)
+
+ # Leave the Notetaker
+ leave_notetaker(notetaker.data.id)
+
+ except NylasApiError as e:
+ print(f"\nNylas API Error: {str(e)}")
+ print(f"Error details: {e.__dict__}")
+ sys.exit(1)
+ except ValueError as e:
+ print(f"\nConfiguration Error: {str(e)}")
+ sys.exit(1)
+ except Exception as e:
+ print(f"\nUnexpected Error: {str(e)}")
+ print(f"Error type: {type(e)}")
+ print(f"Error details: {e.__dict__}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/notetaker_calendar_demo/README.md b/examples/notetaker_calendar_demo/README.md
new file mode 100644
index 00000000..028bf97b
--- /dev/null
+++ b/examples/notetaker_calendar_demo/README.md
@@ -0,0 +1,50 @@
+# Notetaker Calendar Integration Demo
+
+This demo showcases how to use the Nylas Notetaker API in conjunction with calendar and event APIs to create and manage notes associated with calendar events.
+
+## Features Demonstrated
+
+- Creating notes linked to calendar events
+- Retrieving notes associated with events
+- Managing event-related notes
+- Syncing notes with event updates
+- Using note metadata for event organization
+
+## Prerequisites
+
+- Python 3.8+
+- Nylas Python SDK (local version from this repository)
+- Nylas API credentials (Client ID and Client Secret)
+
+## Setup
+
+1. Install the SDK in development mode:
+```bash
+# From the root of the nylas-python repository
+pip install -e .
+```
+
+2. Set up your environment variables:
+```bash
+export NYLAS_API_KEY='your_api_key'
+export NYLAS_API_URI='https://api.nylas.com' # Optional, defaults to https://api.nylas.com
+```
+
+## Running the Demo
+
+From the root of the repository:
+```bash
+python examples/notetaker_calendar_demo/notetaker_calendar_demo.py
+```
+
+## Code Examples
+
+The demo includes examples of:
+
+1. Creating a calendar event with associated notes
+2. Retrieving notes linked to specific events
+3. Updating event notes when the event changes
+4. Managing note metadata for event organization
+5. Syncing notes across multiple events
+
+Each example is documented with comments explaining the functionality and expected output.
\ No newline at end of file
diff --git a/examples/notetaker_calendar_demo/notetaker_calendar_demo.py b/examples/notetaker_calendar_demo/notetaker_calendar_demo.py
new file mode 100644
index 00000000..022f9ded
--- /dev/null
+++ b/examples/notetaker_calendar_demo/notetaker_calendar_demo.py
@@ -0,0 +1,178 @@
+import os
+from datetime import datetime, timedelta
+from typing import Optional
+
+from nylas import Client
+from nylas.models.notetakers import Notetaker
+from nylas.models.events import (
+ UpdateEventRequest,
+ CreateEventRequest,
+ EventNotetakerRequest,
+ EventNotetakerSettings,
+ CreateTimespan,
+ CreateEventQueryParams,
+ UpdateEventQueryParams,
+ CreateAutocreate,
+ CreateEventNotetaker
+)
+
+# Initialize the Nylas client
+nylas = Client(
+ api_key=os.getenv("NYLAS_API_KEY"),
+ api_uri=os.getenv("NYLAS_API_URI", "https://api.us.nylas.com")
+)
+
+def create_event_with_notetaker():
+ """Demonstrates how to create a calendar event with a Notetaker bot."""
+ print("\n=== Creating Event with Notetaker ===")
+
+ # Create the event
+ start_time = datetime.now() + timedelta(days=1)
+ end_time = start_time + timedelta(hours=1)
+
+
+ # Create the request body with proper types
+ request_body = CreateEventRequest(
+ title="Project Planning Meeting",
+ description="Initial project planning and resource allocation",
+ when=CreateTimespan(
+ start_time=int(start_time.timestamp()),
+ end_time=int(end_time.timestamp())
+ ),
+ metadata={
+ "project_id": "PROJ-123",
+ "priority": "high"
+ },
+ conferencing=CreateAutocreate(
+ provider="Google Meet",
+ autocreate={}
+ ),
+ notetaker=CreateEventNotetaker(
+ name="Nylas Notetaker",
+ meeting_settings=EventNotetakerSettings(
+ video_recording=True,
+ audio_recording=True,
+ transcription=True
+ )
+ )
+ )
+
+ # Create the query parameters
+ query_params = CreateEventQueryParams(
+ calendar_id=os.getenv("NYLAS_CALENDAR_ID")
+ )
+
+ event = nylas.events.create(
+ identifier=os.getenv("NYLAS_GRANT_ID"),
+ request_body=request_body,
+ query_params=query_params
+ )
+
+ return event
+
+
+def get_event_notetaker(event_id: str) -> Optional[Notetaker]:
+ """Demonstrates how to retrieve the Notetaker associated with an event."""
+ print("\n=== Retrieving Event Notetaker ===")
+
+ # First get the event to get the Notetaker ID
+ try:
+ event = nylas.events.find(
+ identifier=os.getenv("NYLAS_GRANT_ID"),
+ event_id=event_id,
+ query_params={"calendar_id": os.getenv("NYLAS_CALENDAR_ID")}
+ )
+ except Exception as e:
+ print(f"Error getting event: {e}")
+ return None
+
+ if not event.data.notetaker or not event.data.notetaker.id:
+ print(f"No Notetaker found for event {event_id}")
+ return None
+
+ notetaker = nylas.notetakers.find(notetaker_id=event.data.notetaker.id, identifier=os.getenv("NYLAS_GRANT_ID"))
+ print(f"Found Notetaker for event {event_id}:")
+ print(f"- ID: {notetaker.data.id}")
+ print(f"- State: {notetaker.data.state}")
+ print(f"- Meeting Provider: {notetaker.data.meeting_provider}")
+ print(f"- Meeting Settings:")
+ print(f" - Video Recording: {notetaker.data.meeting_settings.video_recording}")
+ print(f" - Audio Recording: {notetaker.data.meeting_settings.audio_recording}")
+ print(f" - Transcription: {notetaker.data.meeting_settings.transcription}")
+
+ return notetaker
+
+def update_event_and_notetaker(event_id: str, notetaker_id: str):
+ """Demonstrates how to update both an event and its Notetaker."""
+ print("\n=== Updating Event and Notetaker ===")
+
+ # Create the notetaker meeting settings
+ notetaker_settings = EventNotetakerSettings(
+ video_recording=False,
+ audio_recording=True,
+ transcription=False
+ )
+
+ # Create the notetaker request
+ notetaker = EventNotetakerRequest(
+ id=notetaker_id,
+ name="Updated Nylas Notetaker",
+ meeting_settings=notetaker_settings
+ )
+
+ # Create the update request with proper types
+ request_body = UpdateEventRequest(
+ title="Updated Project Planning Meeting",
+ description="Revised project planning with new timeline",
+ metadata={
+ "project_id": "PROJ-123",
+ "priority": "urgent"
+ },
+ notetaker=notetaker
+ )
+
+ # Create the query parameters
+ query_params = UpdateEventQueryParams(
+ calendar_id=os.getenv("NYLAS_CALENDAR_ID")
+ )
+
+ updated_event = nylas.events.update(
+ identifier=os.getenv("NYLAS_GRANT_ID"),
+ event_id=event_id,
+ request_body=request_body,
+ query_params=query_params
+ )
+
+ return updated_event
+
+def main():
+ """Main function to run all demo examples."""
+ try:
+ # Create an event with a Notetaker
+ event = create_event_with_notetaker()
+ if not event:
+ print("Failed to create event")
+ return
+
+ print(f"Created event with ID: {event.data.id}")
+ print(f"Event Notetaker ID: {event.data.notetaker.id}")
+
+ # Get the Notetaker for the event
+ notetaker = get_event_notetaker(event.data.id)
+ if not notetaker:
+ print(f"Failed to get Notetaker for event {event.data.id}")
+ return
+
+ # Update both the event and its Notetaker
+ updated_event = update_event_and_notetaker(event.data.id, notetaker.data.id)
+ if not updated_event:
+ print(f"Failed to update event {event.data.id}")
+ return
+
+ print(f"Updated event with ID: {updated_event.data.id}")
+
+ except Exception as e:
+ print(f"An error occurred: {str(e)}")
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/provider_error_demo/README.md b/examples/provider_error_demo/README.md
new file mode 100644
index 00000000..930a7236
--- /dev/null
+++ b/examples/provider_error_demo/README.md
@@ -0,0 +1,72 @@
+# Provider Error Handling Example
+
+This example demonstrates how to properly handle provider errors when working with the Nylas API. It specifically shows how to catch and process errors that occur when trying to access a non-existent calendar.
+
+## Features
+
+- Demonstrates proper error handling for Nylas API provider errors
+- Shows how to access error details including:
+ - Error message
+ - Error type
+ - Provider error message
+ - Request ID
+ - Status code
+- Includes clear output and status messages
+
+## Prerequisites
+
+1. A Nylas account with API access
+2. Python 3.x installed
+3. Local installation of the Nylas Python SDK (this repository)
+
+## Setup
+
+1. Install the SDK in development mode from the repository root:
+```bash
+cd /path/to/nylas-python
+pip install -e .
+```
+
+2. Set your environment variables:
+```bash
+export NYLAS_API_KEY="your_api_key"
+export NYLAS_GRANT_ID="your_grant_id"
+```
+
+3. Run the example from the repository root:
+```bash
+python examples/provider_error_demo/provider_error_example.py
+```
+
+## Example Output
+
+```
+Demonstrating Provider Error Handling
+====================================
+
+Attempting to fetch events from non-existent calendar: non-existent-calendar-123
+------------------------------------------------------------------
+
+Caught NylasApiError:
+โ Error Message: Calendar not found
+โ Error Type: invalid_request_error
+โ Provider Error: The calendar ID provided does not exist
+โ Request ID: req-abc-123
+โ Status Code: 404
+
+Example completed!
+```
+
+## Error Handling
+
+The example demonstrates how to handle:
+- Missing environment variables
+- API authentication errors
+- Provider-specific errors
+- Non-existent resource errors
+
+## Documentation
+
+For more information about the Nylas Python SDK and its features, visit:
+- [Nylas Python SDK Documentation](https://developer.nylas.com/docs/sdks/python/)
+- [Nylas API Reference](https://developer.nylas.com/docs/api/)
\ No newline at end of file
diff --git a/examples/provider_error_demo/provider_error_example.py b/examples/provider_error_demo/provider_error_example.py
new file mode 100644
index 00000000..3289a485
--- /dev/null
+++ b/examples/provider_error_demo/provider_error_example.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Handling Provider Errors
+
+This example demonstrates how to handle provider errors when working with the Nylas API,
+specifically when trying to access a non-existent calendar.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/provider_error_demo/provider_error_example.py
+"""
+
+import os
+import sys
+from typing import Optional
+
+from nylas import Client
+from nylas.models.errors import NylasApiError
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def demonstrate_provider_error(client: Client, grant_id: str) -> None:
+ """Demonstrate how to handle provider errors."""
+ # Use a non-existent calendar ID to trigger a provider error
+ non_existent_calendar_id = "non-existent-calendar-123"
+
+ try:
+ print(f"\nAttempting to fetch events from non-existent calendar: {non_existent_calendar_id}")
+ print("------------------------------------------------------------------")
+
+ # Attempt to list events with the invalid calendar ID
+ events, request_id = client.events.list(
+ identifier=grant_id,
+ query_params={"calendar_id": non_existent_calendar_id}
+ )
+
+ # Note: We won't reach this code due to the error
+ print("Events retrieved:", events)
+
+ except NylasApiError as e:
+ print("\nCaught NylasApiError:")
+ print(f"โ Error Type: {e.type}")
+ print(f"โ Provider Error: {e.provider_error}")
+ print(f"โ Request ID: {e.request_id}")
+ print(f"โ Status Code: {e.status_code}")
+
+
+def main():
+ """Main function demonstrating provider error handling."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(
+ api_key=api_key,
+ )
+
+ print("\nDemonstrating Provider Error Handling")
+ print("====================================")
+
+ # Demonstrate provider error handling
+ demonstrate_provider_error(client, grant_id)
+
+ print("\nExample completed!")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/response_headers_demo/README.md b/examples/response_headers_demo/README.md
new file mode 100644
index 00000000..8ba6959d
--- /dev/null
+++ b/examples/response_headers_demo/README.md
@@ -0,0 +1,107 @@
+# Response Headers Demo
+
+This example demonstrates how to access and use response headers from various Nylas API responses. It shows how headers are available in different types of responses:
+
+1. List responses (from methods like `list()`)
+2. Single-item responses (from methods like `find()`)
+3. Error responses (when API calls fail)
+
+## What You'll Learn
+
+- How to access response headers from successful API calls
+- How to access headers from error responses
+- Common headers you'll encounter in Nylas API responses
+- How headers differ between list and single-item responses
+
+## Headers Demonstrated
+
+The example will show various headers that Nylas includes in responses, such as:
+
+- `request-id`: Unique identifier for the API request
+- `x-ratelimit-limit`: Your rate limit for the endpoint
+- `x-ratelimit-remaining`: Remaining requests within the current window
+- `x-ratelimit-reset`: When the rate limit window resets
+- And more...
+
+## Prerequisites
+
+Before running this example, make sure you have:
+
+1. A Nylas API key
+2. A Nylas grant ID
+3. Python 3.7 or later installed
+4. The Nylas Python SDK installed
+
+## Setup
+
+1. First, install the SDK in development mode:
+ ```bash
+ cd /path/to/nylas-python
+ pip install -e .
+ ```
+
+2. Set up your environment variables:
+ ```bash
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ ```
+
+## Running the Example
+
+Run the example with:
+```bash
+python examples/response_headers_demo/response_headers_example.py
+```
+
+The script will:
+1. Demonstrate headers from a list response by fetching messages
+2. Show headers from a single-item response by fetching one message
+3. Trigger and catch an error to show error response headers
+
+## Example Output
+
+You'll see output similar to this:
+
+```
+Demonstrating Response Headers
+============================
+
+Demonstrating List Response Headers
+----------------------------------
+โ Successfully retrieved messages
+
+Response Headers:
+------------------------
+request-id: req_abcd1234
+x-ratelimit-limit: 1000
+x-ratelimit-remaining: 999
+...
+
+Demonstrating Find Response Headers
+----------------------------------
+โ Successfully retrieved single message
+
+Response Headers:
+------------------------
+request-id: req_efgh5678
+...
+
+Demonstrating Error Response Headers
+---------------------------------
+โ Successfully caught expected error
+โ Error Type: invalid_request
+โ Request ID: req_ijkl9012
+โ Status Code: 404
+
+Error Response Headers:
+------------------------
+request-id: req_ijkl9012
+...
+```
+
+## Error Handling
+
+The example includes proper error handling and will show you how to:
+- Catch `NylasApiError` exceptions
+- Access error details and headers
+- Handle different types of API errors gracefully
\ No newline at end of file
diff --git a/examples/response_headers_demo/response_headers_example.py b/examples/response_headers_demo/response_headers_example.py
new file mode 100644
index 00000000..d93d8b04
--- /dev/null
+++ b/examples/response_headers_demo/response_headers_example.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Response Headers Demo
+
+This example demonstrates how to access and use response headers from various Nylas API
+responses, including successful responses and error cases.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/response_headers_demo/response_headers_example.py
+"""
+
+import os
+import sys
+from typing import Optional
+
+from nylas import Client
+from nylas.models.errors import NylasApiError
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_response_headers(headers: dict, prefix: str = "") -> None:
+ """Helper function to print response headers."""
+ print(f"\n{prefix} Response Headers:")
+ print("------------------------")
+ for key, value in headers.items():
+ print(f"{key}: {value}")
+
+
+def demonstrate_list_response_headers(client: Client, grant_id: str) -> None:
+ """Demonstrate headers in list responses."""
+ print("\nDemonstrating List Response Headers")
+ print("----------------------------------")
+
+ try:
+ # List messages to get a ListResponse
+ messages = client.messages.list(identifier=grant_id)
+
+ print("โ Successfully retrieved messages")
+ print_response_headers(messages.headers)
+ print(f"Total messages count: {len(messages.data)}")
+
+ except NylasApiError as e:
+ print("\nError occurred while listing messages:")
+ print(f"โ Error Type: {e.type}")
+ print(f"โ Provider Error: {e.provider_error}")
+ print(f"โ Request ID: {e.request_id}")
+ print_response_headers(e.headers, "Error")
+
+
+def demonstrate_list_response_headers_with_pagination(client: Client, grant_id: str) -> None:
+ """Demonstrate headers in list responses with pagination."""
+ print("\nDemonstrating List Response Headers with Pagination")
+ print("--------------------------------------------------")
+
+ try:
+ # List messages to get a ListResponse
+ threads = client.threads.list(identifier=grant_id, query_params={"limit": 1})
+
+ print("โ Successfully retrieved threads")
+ print_response_headers(threads.headers)
+ print(f"Total threads count: {len(threads.data)}")
+
+ except NylasApiError as e:
+ print("\nError occurred while listing threads:")
+ print(f"โ Error Type: {e.type}")
+ print(f"โ Provider Error: {e.provider_error}")
+ print(f"โ Request ID: {e.request_id}")
+ print_response_headers(e.headers, "Error")
+
+
+def demonstrate_find_response_headers(client: Client, grant_id: str) -> None:
+ """Demonstrate headers in find/single-item responses."""
+ print("\nDemonstrating Find Response Headers")
+ print("----------------------------------")
+
+ try:
+ # Get the first message to demonstrate single-item response
+ messages = client.messages.list(identifier=grant_id)
+ if not messages.data:
+ print("No messages found to demonstrate find response")
+ return
+
+ message_id = messages.data[0].id
+ message = client.messages.find(identifier=grant_id, message_id=message_id)
+
+ print("โ Successfully retrieved single message")
+ print_response_headers(message.headers)
+
+ except NylasApiError as e:
+ print("\nError occurred while finding message:")
+ print(f"โ Error Type: {e.type}")
+ print(f"โ Provider Error: {e.provider_error}")
+ print(f"โ Request ID: {e.request_id}")
+ print_response_headers(e.headers, "Error")
+
+
+def demonstrate_error_response_headers(client: Client, grant_id: str) -> None:
+ """Demonstrate headers in error responses."""
+ print("\nDemonstrating Error Response Headers")
+ print("---------------------------------")
+
+ try:
+ # Attempt to find a non-existent message
+ message = client.messages.find(
+ identifier=grant_id,
+ message_id="non-existent-id-123"
+ )
+
+ except NylasApiError as e:
+ print("โ Successfully caught expected error")
+ print(f"โ Error Type: {e.type}")
+ print(f"โ Provider Error: {e.provider_error}")
+ print(f"โ Request ID: {e.request_id}")
+ print(f"โ Status Code: {e.status_code}")
+ print_response_headers(e.headers, "Error")
+
+
+def main():
+ """Main function demonstrating response headers."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(
+ api_key=api_key,
+ )
+
+ print("\nDemonstrating Response Headers")
+ print("============================")
+
+ # Demonstrate different types of responses and their headers
+ demonstrate_list_response_headers(client, grant_id)
+ demonstrate_list_response_headers_with_pagination(client, grant_id)
+ demonstrate_find_response_headers(client, grant_id)
+ demonstrate_error_response_headers(client, grant_id)
+
+ print("\nExample completed!")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/select_param_demo/README.md b/examples/select_param_demo/README.md
new file mode 100644
index 00000000..8277ae58
--- /dev/null
+++ b/examples/select_param_demo/README.md
@@ -0,0 +1,58 @@
+# Select Parameter Demo
+
+This example demonstrates the usage of the `select` query parameter across different Nylas resources. The `select` parameter allows you to specify which fields you want to receive in the API response, helping to optimize your API calls by reducing the amount of data transferred.
+
+## Features Demonstrated
+
+1. **Backwards Compatibility**: Shows that existing code that doesn't use the `select` parameter continues to work as expected, receiving all fields.
+2. **Field Selection**: Demonstrates how to use the `select` parameter to request only specific fields for better performance.
+3. **Multiple Resources**: Shows the `select` parameter working across different resources:
+ - Messages
+ - Calendars
+ - Events
+ - Drafts
+ - Contacts
+
+## Setup
+
+1. Create a `.env` file in the root directory with your Nylas API credentials:
+ ```
+ NYLAS_API_KEY=your_api_key_here
+ ```
+
+2. Install the required dependencies:
+ ```bash
+ pip install nylas python-dotenv
+ ```
+
+## Running the Example
+
+Run the example script:
+```bash
+python select_param_example.py
+```
+
+The script will demonstrate both the traditional way of fetching all fields and the new selective field fetching for each resource type.
+
+## Example Output
+
+The script will show output similar to this for each resource:
+```
+=== Messages Resource ===
+
+Fetching messages (all fields):
+Full message - Subject: Example Subject, ID: abc123...
+
+Fetching messages with select (only id and subject):
+Minimal message - Subject: Example Subject, ID: abc123...
+```
+
+## Benefits of Using Select
+
+1. **Reduced Data Transfer**: By selecting only the fields you need, you reduce the amount of data transferred over the network.
+2. **Improved Performance**: Smaller payloads mean faster API responses and less processing time.
+3. **Bandwidth Optimization**: Especially useful in mobile applications or when dealing with limited bandwidth.
+
+## Available Fields
+
+The fields available for selection vary by resource type. Refer to the [Nylas API documentation](https://developer.nylas.com/) for a complete list of available fields for each resource type.
\ No newline at end of file
diff --git a/examples/select_param_demo/select_param_example.py b/examples/select_param_demo/select_param_example.py
new file mode 100644
index 00000000..76ad8b6b
--- /dev/null
+++ b/examples/select_param_demo/select_param_example.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Using Select Parameters
+
+This example demonstrates how to use the 'select' query parameter across different Nylas resources
+to optimize API response size and performance by requesting only specific fields.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ python examples/select_param_demo/select_param_example.py
+"""
+
+import os
+import sys
+import json
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_data(data: list, title: str) -> None:
+ """Pretty print the data with a title."""
+ print(f"\n{title}:")
+ for item in data:
+ # Convert to dict and pretty print
+ item_dict = item.to_dict()
+ print(json.dumps(item_dict, indent=2))
+
+
+def demonstrate_messages(client: Client, grant_id: str) -> None:
+ """Demonstrate select parameter usage with Messages resource."""
+ print("\n=== Messages Resource ===")
+
+ # Backwards compatibility - fetch all fields
+ print("\nFetching messages (all fields):")
+ messages = client.messages.list(identifier=grant_id, query_params={"limit": 2})
+ print_data(messages.data, "Full message data (all fields)")
+
+ # Using select parameter - fetch only specific fields
+ print("\nFetching messages with select (only id and subject):")
+ messages = client.messages.list(
+ identifier=grant_id,
+ query_params={"limit": 2, "select": "id,subject"}
+ )
+ print_data(messages.data, "Minimal message data (only selected fields)")
+
+
+def demonstrate_calendars(client: Client, grant_id: str) -> None:
+ """Demonstrate select parameter usage with Calendars resource."""
+ print("\n=== Calendars Resource ===")
+
+ # Backwards compatibility - fetch all fields
+ print("\nFetching calendars (all fields):")
+ calendars = client.calendars.list(identifier=grant_id, query_params={"limit": 2})
+ print_data(calendars.data, "Full calendar data (all fields)")
+
+ # Using select parameter - fetch only specific fields
+ print("\nFetching calendars with select (only id and name):")
+ calendars = client.calendars.list(
+ identifier=grant_id,
+ query_params={"limit": 2, "select": "id,name"}
+ )
+ print_data(calendars.data, "Minimal calendar data (only selected fields)")
+
+
+def demonstrate_events(client: Client, grant_id: str) -> None:
+ """Demonstrate select parameter usage with Events resource."""
+ print("\n=== Events Resource ===")
+
+ # First, get a calendar ID
+ print("\nFetching first calendar to use for events...")
+ calendars = client.calendars.list(identifier=grant_id, query_params={"limit": 1})
+ if not calendars.data:
+ print("No calendars found. Skipping events demonstration.")
+ return
+
+ calendar_id = calendars.data[0].id
+ print(f"Using calendar: {calendars.data[0].name} (ID: {calendar_id})")
+
+ # Backwards compatibility - fetch all fields
+ print("\nFetching events (all fields):")
+ events = client.events.list(
+ identifier=grant_id,
+ query_params={"limit": 2, "calendar_id": calendar_id}
+ )
+ print_data(events.data, "Full event data (all fields)")
+
+ # Using select parameter - fetch only specific fields
+ print("\nFetching events with select (only id and title):")
+ events = client.events.list(
+ identifier=grant_id,
+ query_params={
+ "limit": 2,
+ "calendar_id": calendar_id,
+ "select": "id,title"
+ }
+ )
+ print_data(events.data, "Minimal event data (only selected fields)")
+
+
+def demonstrate_drafts(client: Client, grant_id: str) -> None:
+ """Demonstrate select parameter usage with Drafts resource."""
+ print("\n=== Drafts Resource ===")
+
+ # Backwards compatibility - fetch all fields
+ print("\nFetching drafts (all fields):")
+ drafts = client.drafts.list(identifier=grant_id, query_params={"limit": 2})
+ print_data(drafts.data, "Full draft data (all fields)")
+
+ # Using select parameter - fetch only specific fields
+ print("\nFetching drafts with select (only id and subject):")
+ drafts = client.drafts.list(
+ identifier=grant_id,
+ query_params={"limit": 2, "select": "id,subject"}
+ )
+ print_data(drafts.data, "Minimal draft data (only selected fields)")
+
+
+def demonstrate_contacts(client: Client, grant_id: str) -> None:
+ """Demonstrate select parameter usage with Contacts resource."""
+ print("\n=== Contacts Resource ===")
+
+ # Backwards compatibility - fetch all fields
+ print("\nFetching contacts (all fields):")
+ contacts = client.contacts.list(identifier=grant_id, query_params={"limit": 2})
+ print_data(contacts.data, "Full contact data (all fields)")
+
+ # Using select parameter - fetch only specific fields
+ print("\nFetching contacts with select (only id, grant_id, and given_name):")
+ contacts = client.contacts.list(
+ identifier=grant_id,
+ query_params={"limit": 2, "select": "id,grant_id,given_name"}
+ )
+ print_data(contacts.data, "Minimal contact data (only selected fields)")
+
+
+def main():
+ """Main function demonstrating select parameter usage across resources."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+
+ # Initialize Nylas client
+ client = Client(
+ api_key=api_key,
+ )
+
+ print("\nDemonstrating Select Parameter Usage")
+ print("===================================")
+ print("This shows both backwards compatibility and selective field fetching")
+
+ # Demonstrate select parameter across different resources
+ demonstrate_messages(client, grant_id)
+ demonstrate_calendars(client, grant_id)
+ demonstrate_events(client, grant_id)
+ demonstrate_drafts(client, grant_id)
+ demonstrate_contacts(client, grant_id)
+
+ print("\nExample completed!")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/examples/send_email_demo/README.md b/examples/send_email_demo/README.md
new file mode 100644
index 00000000..fa5edfe4
--- /dev/null
+++ b/examples/send_email_demo/README.md
@@ -0,0 +1,77 @@
+# Send Email Example
+
+This example demonstrates how to send an email with special characters (accented letters) in the subject line using the Nylas Python SDK.
+
+## Overview
+
+The example sends an email with the subject **"De l'idรฉe ร la post-prod, sans friction"** to demonstrate proper handling of UTF-8 characters in email subjects.
+
+## Prerequisites
+
+- Python 3.8 or higher
+- Nylas Python SDK installed
+- Nylas API key
+- Nylas grant ID
+- Email address for testing
+
+## Setup
+
+1. Install the SDK in development mode:
+ ```bash
+ cd /path/to/nylas-python
+ pip install -e .
+ ```
+
+2. Set the required environment variables:
+ ```bash
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ export RECIPIENT_EMAIL="recipient@example.com"
+ ```
+
+## Running the Example
+
+```bash
+python examples/send_email_demo/send_email_example.py
+```
+
+## What This Example Demonstrates
+
+- Sending an email with special characters (accented letters) in the subject
+- Proper UTF-8 encoding of email subjects
+- Using the `messages.send()` method to send emails directly
+
+## Expected Output
+
+```
+============================================================
+ Nylas SDK: Send Email with Special Characters Example
+============================================================
+
+This example sends an email with the subject:
+ "De l'idรฉe ร la post-prod, sans friction"
+
+Grant ID: your_grant_id
+Recipient: recipient@example.com
+
+Sending email...
+ To: recipient@example.com
+ Subject: De l'idรฉe ร la post-prod, sans friction
+
+โ Email sent successfully!
+ Message ID: message-id-here
+ Subject: De l'idรฉe ร la post-prod, sans friction
+
+โ Special characters in subject are correctly preserved
+
+============================================================
+Example completed successfully! โ
+============================================================
+```
+
+## Notes
+
+- The SDK properly handles UTF-8 characters in email subjects and bodies
+- Special characters like รฉ, ร , and other accented letters are preserved correctly
+- The email will be delivered with the subject exactly as specified
+
diff --git a/examples/send_email_demo/send_email_example.py b/examples/send_email_demo/send_email_example.py
new file mode 100644
index 00000000..6edf46dc
--- /dev/null
+++ b/examples/send_email_demo/send_email_example.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Send Email with Special Characters
+
+This example demonstrates how to send an email with special characters
+(accented letters) in the subject line using the Nylas Python SDK.
+
+The example sends an email with the subject "De l'idรฉe ร la post-prod, sans friction"
+to demonstrate proper handling of UTF-8 characters in email subjects.
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+ RECIPIENT_EMAIL: Email address to send the message to
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ export RECIPIENT_EMAIL="recipient@example.com"
+ python examples/send_email_demo/send_email_example.py
+"""
+
+import os
+import sys
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def send_email(client: Client, grant_id: str, recipient: str) -> None:
+ """Send an email with special characters in the subject."""
+ # Subject with special characters (accented letters)
+ subject = "De l'idรฉe ร la post-prod, sans friction"
+
+ body = """
+
+
+
Bonjour!
+
Ce message dรฉmontre l'envoi d'un email avec des caractรจres spรฉciaux dans le sujet.
+
Le sujet de cet email est: De l'idรฉe ร la post-prod, sans friction
+
Les caractรจres accentuรฉs sont correctement prรฉservรฉs grรขce ร l'encodage UTF-8.
+
+
+ """
+
+ print(f"Sending email...")
+ print(f" To: {recipient}")
+ print(f" Subject: {subject}")
+
+ try:
+ response = client.messages.send(
+ identifier=grant_id,
+ request_body={
+ "subject": subject,
+ "to": [{"email": recipient}],
+ "body": body,
+ }
+ )
+
+ print(f"\nโ Email sent successfully!")
+ print(f" Message ID: {response.data.id}")
+ print(f" Subject: {response.data.subject}")
+ print(f"\nโ Special characters in subject are correctly preserved")
+
+ except Exception as e:
+ print(f"\nโ Error sending email: {e}")
+ sys.exit(1)
+
+
+def main():
+ """Main function."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+ recipient = get_env_or_exit("RECIPIENT_EMAIL")
+
+ # Initialize Nylas client
+ client = Client(api_key=api_key)
+
+ print("=" * 60)
+ print(" Nylas SDK: Send Email with Special Characters Example")
+ print("=" * 60)
+ print()
+ print("This example sends an email with the subject:")
+ print(' "De l\'idรฉe ร la post-prod, sans friction"')
+ print()
+ print(f"Grant ID: {grant_id}")
+ print(f"Recipient: {recipient}")
+ print()
+
+ # Send the email
+ send_email(client, grant_id, recipient)
+
+ print("\n" + "=" * 60)
+ print("Example completed successfully! โ ")
+ print("=" * 60)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/examples/special_characters_demo/README.md b/examples/special_characters_demo/README.md
new file mode 100644
index 00000000..bbbdce9d
--- /dev/null
+++ b/examples/special_characters_demo/README.md
@@ -0,0 +1,121 @@
+# Special Characters Encoding Example
+
+This example demonstrates how the Nylas Python SDK correctly handles special characters (accented letters, unicode characters) in email subjects and message bodies.
+
+## The Problem
+
+Previously, when sending emails with large attachments (>3MB), special characters in the subject line would be incorrectly encoded. For example:
+
+- **Intended Subject:** "De l'idรฉe ร la post-prod, sans friction"
+- **What Recipients Saw:** "De lรขโฌโขidรฉe ร la post-prod, sans friction"
+
+This issue occurred because the SDK was using `json.dumps()` with the default `ensure_ascii=True` parameter when creating multipart/form-data requests for large attachments.
+
+## The Solution
+
+The SDK now uses `json.dumps(request_body, ensure_ascii=False)` to preserve UTF-8 characters correctly in the JSON payload, ensuring that special characters are displayed properly in recipient inboxes.
+
+## What This Example Demonstrates
+
+1. **Small Messages** - Sending messages with special characters (no attachments)
+2. **Large Messages** - Sending messages with special characters AND large attachments (>3MB)
+3. **Drafts** - Creating drafts with special characters
+4. **International Support** - Handling various international character sets
+
+## Usage
+
+### Prerequisites
+
+1. Install the SDK in development mode:
+ ```bash
+ cd /path/to/nylas-python
+ pip install -e .
+ ```
+
+2. Set up environment variables:
+ ```bash
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ export RECIPIENT_EMAIL="recipient@example.com"
+ ```
+
+### Run the Example
+
+```bash
+python examples/special_characters_demo/special_characters_example.py
+```
+
+## Test Coverage
+
+This fix is covered by comprehensive tests:
+
+```bash
+# Test the core fix in file_utils
+pytest tests/utils/test_file_utils.py::TestFileUtils::test_build_form_request_with_special_characters
+
+# Test message sending with special characters
+pytest tests/resources/test_messages.py::TestMessage::test_send_message_with_special_characters_in_subject
+pytest tests/resources/test_messages.py::TestMessage::test_send_message_with_special_characters_large_attachment
+
+# Test draft creation with special characters
+pytest tests/resources/test_drafts.py::TestDraft::test_create_draft_with_special_characters_in_subject
+pytest tests/resources/test_drafts.py::TestDraft::test_create_draft_with_special_characters_large_attachment
+```
+
+## Supported Character Sets
+
+The SDK correctly handles:
+
+- **French:** รฉ, รจ, รช, ร , รน, รง, ล
+- **Spanish:** รฑ, รก, รญ, รณ, รบ, ยฟ, ยก
+- **German:** รค, รถ, รผ, ร
+- **Portuguese:** รฃ, รต, รข, รช
+- **Italian:** ร , รจ, รฉ, รฌ, รฒ, รน
+- **Russian:** Cyrillic characters
+- **Japanese:** Hiragana, Katakana, Kanji
+- **Chinese:** Simplified and Traditional characters
+- **Emoji:** ๐ ๐ ๐ฅณ and many more
+- **Special symbols:** โฌ, ยฃ, ยฅ, ยฉ, ยฎ, โข
+
+## Technical Details
+
+### The Bug
+
+When using multipart/form-data encoding (for large attachments), the message payload was serialized as:
+
+```python
+message_payload = json.dumps(request_body) # Default: ensure_ascii=True
+```
+
+This caused special characters to be escaped as unicode sequences:
+```json
+{"subject": "De l\u2019id\u00e9e"}
+```
+
+### The Fix
+
+The payload is now serialized as:
+
+```python
+message_payload = json.dumps(request_body, ensure_ascii=False)
+```
+
+This preserves the actual UTF-8 characters:
+```json
+{"subject": "De l'idรฉe"}
+```
+
+The multipart/form-data Content-Type header correctly specifies UTF-8 encoding, ensuring email clients display the characters properly.
+
+## Related Files
+
+- **Core Fix:** `nylas/utils/file_utils.py` - Line 70
+- **Tests:** `tests/utils/test_file_utils.py`, `tests/resources/test_messages.py`, `tests/resources/test_drafts.py`
+- **Example:** `examples/special_characters_demo/special_characters_example.py`
+
+## Impact
+
+โ **Before Fix:** Special characters in subjects were garbled when sending emails with large attachments
+โ **After Fix:** All special characters are correctly preserved and displayed
+
+The fix ensures backwards compatibility - all existing code continues to work without changes.
diff --git a/examples/special_characters_demo/special_characters_example.py b/examples/special_characters_demo/special_characters_example.py
new file mode 100755
index 00000000..e112e332
--- /dev/null
+++ b/examples/special_characters_demo/special_characters_example.py
@@ -0,0 +1,325 @@
+#!/usr/bin/env python3
+"""
+Nylas SDK Example: Handling Special Characters in Email Subjects and Bodies
+
+This example demonstrates proper handling of special characters (accented letters,
+unicode characters) in email subjects and message bodies, particularly when sending
+messages with large attachments.
+
+The SDK now correctly preserves UTF-8 characters in email subjects and bodies,
+preventing encoding issues like "De l'idรฉe ร la post-prod" becoming
+"De lรยขรโฌรโขidรฦรยฉe รฦร la post-prod".
+
+Required Environment Variables:
+ NYLAS_API_KEY: Your Nylas API key
+ NYLAS_GRANT_ID: Your Nylas grant ID
+ RECIPIENT_EMAIL: Email address to send test messages to
+
+Usage:
+ First, install the SDK in development mode:
+ cd /path/to/nylas-python
+ pip install -e .
+
+ Then set environment variables and run:
+ export NYLAS_API_KEY="your_api_key"
+ export NYLAS_GRANT_ID="your_grant_id"
+ export RECIPIENT_EMAIL="recipient@example.com"
+ python examples/special_characters_demo/special_characters_example.py
+"""
+
+import os
+import sys
+import io
+from nylas import Client
+
+
+def get_env_or_exit(var_name: str) -> str:
+ """Get an environment variable or exit if not found."""
+ value = os.getenv(var_name)
+ if not value:
+ print(f"Error: {var_name} environment variable is required")
+ sys.exit(1)
+ return value
+
+
+def print_separator(title: str) -> None:
+ """Print a formatted section separator."""
+ print(f"\n{'='*60}")
+ print(f" {title}")
+ print('='*60)
+
+
+def demonstrate_small_message_with_special_chars(client: Client, grant_id: str, recipient: str) -> None:
+ """Demonstrate sending a message with special characters (no attachments)."""
+ print_separator("Sending Message with Special Characters (No Attachments)")
+
+ try:
+ # This is the exact subject from the bug report
+ subject = "De l'idรฉe ร la post-prod, sans friction"
+ body = """
+
+
+
+
+
+ """
+
+ print(f"Subject: {subject}")
+ print(f"To: {recipient}")
+ print("Body contains various special characters...")
+
+ print("\nSending message...")
+ response = client.messages.send(
+ identifier=grant_id,
+ request_body={
+ "subject": subject,
+ "to": [{"email": recipient}],
+ "body": body,
+ }
+ )
+
+ print(f"โ Message sent successfully!")
+ print(f" Message ID: {response.data.id}")
+ print(f" Subject preserved: {response.data.subject == subject}")
+ print(f"\nโ Special characters in subject and body are correctly encoded")
+
+ except Exception as e:
+ print(f"โ Error sending message: {e}")
+
+
+def demonstrate_message_with_large_attachment(client: Client, grant_id: str, recipient: str) -> None:
+ """Demonstrate sending a message with special characters AND large attachment."""
+ print_separator("Message with Special Characters + Large Attachment")
+
+ try:
+ # This is the exact subject from the bug report
+ subject = "De l'idรฉe ร la post-prod, sans friction"
+ body = """
+
+
+
Message avec piรจce jointe volumineuse
+
+ Ce message dรฉmontre que les caractรจres spรฉciaux sont
+ correctement prรฉservรฉs mรชme lors de l'utilisation de
+ multipart/form-data pour les grandes piรจces jointes.
+
+
+
+ """
+
+ # Create a large attachment (>3MB) to trigger multipart/form-data encoding
+ # This is where the encoding bug was happening
+ large_content = b"A" * (3 * 1024 * 1024 + 1000) # Slightly over 3MB
+ attachment_stream = io.BytesIO(large_content)
+
+ print(f"Subject: {subject}")
+ print(f"To: {recipient}")
+ print(f"Attachment size: {len(large_content) / (1024*1024):.2f} MB")
+ print(" (Using multipart/form-data encoding)")
+
+ print("\nSending message with large attachment...")
+ response = client.messages.send(
+ identifier=grant_id,
+ request_body={
+ "subject": subject,
+ "to": [{"email": recipient}],
+ "body": body,
+ "attachments": [
+ {
+ "filename": "large_file.txt",
+ "content_type": "text/plain",
+ "content": attachment_stream,
+ "size": len(large_content),
+ }
+ ],
+ }
+ )
+
+ print(f"โ Message with large attachment sent successfully!")
+ print(f" Message ID: {response.data.id}")
+ print(f" Subject preserved: {response.data.subject == subject}")
+ print(f"\nโ Special characters are correctly encoded even with large attachments!")
+ print(" (The fix ensures ensure_ascii=False in json.dumps for multipart data)")
+
+ except Exception as e:
+ print(f"โ Error sending message with large attachment: {e}")
+
+
+def demonstrate_draft_with_special_chars(client: Client, grant_id: str, recipient: str) -> None:
+ """Demonstrate creating a draft with special characters."""
+ print_separator("Creating Draft with Special Characters")
+
+ try:
+ subject = "Rรฉunion importante: cafรฉ & stratรฉgie"
+ body = """
+
+
+
Ordre du jour
+
+
Rรฉvision du budget (โฌ)
+
Stratรฉgie de dรฉveloppement
+
Cafรฉ et discussion informelle
+
+
ร bientรดt!
+
+
+ """
+
+ print(f"Subject: {subject}")
+ print(f"To: {recipient}")
+
+ print("\nCreating draft...")
+ response = client.drafts.create(
+ identifier=grant_id,
+ request_body={
+ "subject": subject,
+ "to": [{"email": recipient}],
+ "body": body,
+ }
+ )
+
+ print(f"โ Draft created successfully!")
+ print(f" Draft ID: {response.data.id}")
+ print(f" Subject preserved: {response.data.subject == subject}")
+
+ # Clean up - delete the draft
+ print("\nCleaning up draft...")
+ client.drafts.destroy(identifier=grant_id, draft_id=response.data.id)
+ print("โ Draft deleted")
+
+ print(f"\nโ Special characters in drafts are correctly handled")
+
+ except Exception as e:
+ print(f"โ Error with draft: {e}")
+
+
+def demonstrate_various_languages(client: Client, grant_id: str, recipient: str) -> None:
+ """Demonstrate various international characters."""
+ print_separator("International Characters - Various Languages")
+
+ test_cases = [
+ ("French", "Rรฉservation confirmรฉe: cafรฉ ร 15h"),
+ ("Spanish", "ยกHola! ยฟCรณmo estรกs? Maรฑana serรก mejor"),
+ ("German", "Grรถรe: รผber 100 Stรผck verfรผgbar"),
+ ("Portuguese", "Atenรงรฃo: promoรงรฃo vรกlida atรฉ amanhรฃ"),
+ ("Italian", "Caffรจ espresso: รจ cosรฌ buono!"),
+ ("Russian", "ะัะธะฒะตั! ะะฐะบ ะดะตะปะฐ?"),
+ ("Japanese", "ใใใซใกใฏใใๅ ๆฐใงใใ๏ผ"),
+ ("Chinese", "ไฝ ๅฅฝ๏ผๆ่ฟๆไนๆ ท๏ผ"),
+ ("Emoji", "๐ Celebration time! ๐ Let's party ๐ฅณ"),
+ ]
+
+ print("Testing subjects in various languages:")
+ print("(Note: Not actually sending to avoid spam)")
+ print()
+
+ for language, subject in test_cases:
+ print(f" {language:15} : {subject}")
+ # In a real scenario, you could send these
+ # For demo purposes, we just show they can be handled
+
+ print(f"\nโ All international characters can be properly encoded")
+ print(" The SDK preserves UTF-8 encoding correctly")
+
+
+def demonstrate_encoding_explanation() -> None:
+ """Explain the encoding fix."""
+ print_separator("Technical Explanation of the Fix")
+
+ print("""
+The Bug:
+--------
+When sending emails with large attachments (>3MB), the SDK uses
+multipart/form-data encoding. Previously, the message payload was
+serialized using:
+
+ json.dumps(request_body) # Default: ensure_ascii=True
+
+This caused special characters to be escaped as unicode sequences:
+ "De l'idรฉe" โ "De l\\u2019id\\u00e9e"
+
+When Gmail received this, it would sometimes double-decode or misinterpret
+these escape sequences, resulting in:
+ "De lรขโฌโขidรฉe" or similar garbled text
+
+The Fix:
+--------
+The SDK now uses:
+
+ json.dumps(request_body, ensure_ascii=False)
+
+This preserves the actual UTF-8 characters in the JSON payload:
+ "De l'idรฉe" โ "De l'idรฉe" (unchanged)
+
+The multipart/form-data Content-Type header correctly specifies UTF-8,
+so email clients now receive and display the characters correctly.
+
+Impact:
+-------
+โ Small messages (no large attachments): Always worked correctly
+โ Large messages (with attachments >3MB): Now work correctly!
+โ Drafts with large attachments: Now work correctly!
+โ All international characters: Properly preserved
+
+Testing:
+--------
+Run the included tests to verify:
+ pytest tests/utils/test_file_utils.py::TestFileUtils::test_build_form_request_with_special_characters
+ pytest tests/resources/test_messages.py::TestMessage::test_send_message_with_special_characters_large_attachment
+ pytest tests/resources/test_drafts.py::TestDraft::test_create_draft_with_special_characters_large_attachment
+ """)
+
+
+def main():
+ """Main function demonstrating special character handling."""
+ # Get required environment variables
+ api_key = get_env_or_exit("NYLAS_API_KEY")
+ grant_id = get_env_or_exit("NYLAS_GRANT_ID")
+ recipient = get_env_or_exit("RECIPIENT_EMAIL")
+
+ # Initialize Nylas client
+ client = Client(api_key=api_key)
+
+ print("โ" + "="*58 + "โ")
+ print("โ Nylas SDK: Special Characters Encoding Example โ")
+ print("โ" + "="*58 + "โ")
+ print()
+ print("This example demonstrates the fix for email subject/body")
+ print("encoding issues with special characters (accented letters).")
+ print()
+ print(f"Testing with:")
+ print(f" Grant ID: {grant_id}")
+ print(f" Recipient: {recipient}")
+
+ # Demonstrate different scenarios
+ demonstrate_small_message_with_special_chars(client, grant_id, recipient)
+ demonstrate_message_with_large_attachment(client, grant_id, recipient)
+ demonstrate_draft_with_special_chars(client, grant_id, recipient)
+ demonstrate_various_languages(client, grant_id, recipient)
+ demonstrate_encoding_explanation()
+
+ print_separator("Example Completed Successfully! โ ")
+ print("\nKey Takeaways:")
+ print("1. Special characters are now correctly preserved in all email subjects")
+ print("2. The fix applies to both small and large messages (with attachments)")
+ print("3. Drafts also handle special characters correctly")
+ print("4. All international character sets are supported")
+ print()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/webhooks/README.md b/examples/webhooks/README.md
deleted file mode 100644
index ac9483a1..00000000
--- a/examples/webhooks/README.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Example: Webhooks
-
-This is an example project that demonstrates how to use
-[the webhooks feature on Nylas](https://docs.nylas.com/reference#webhooks).
-When you run the app and set up a webhook with Nylas, it will print out
-some information every time you receive a webhook notification from Nylas.
-
-In order to successfully run this example, you need to do the following things:
-
-## Install and run redis
-
-[Redis](https://redis.io/) is an in-memory data store. This example uses it
-as a message broker for the Celery task queue. You'll need to have it running
-on your local computer in order to use the task queue.
-
-If you're using macOS, you can install redis from [Homebrew](https://brew.sh/),
-like this:
-
-```
-brew install redis
-brew services start redis
-```
-
-If you're unable to install and run redis, you can still run this example
-without the task queue -- keep reading.
-
-## Get a client ID & client secret from Nylas
-
-To do this, make a [Nylas Developer](https://developer.nylas.com/) account.
-You should see your client ID and client secret on the dashboard,
-once you've logged in on the
-[Nylas Developer](https://developer.nylas.com/) website.
-
-## Update the `config.json` File
-
-Open the `config.json` file in this directory, and replace the example
-client ID and client secret with the real values that you got from the Nylas
-Developer dashboard. You'll also need to replace the example secret key with
-any random string of letters and numbers: a keyboard mash will do.
-
-The config file also has two options related to Celery, which you probably
-don't need to modify. `CELERY_BROKER_URL` should point to your running redis
-server: if you've got it running on your local computer, you're all set.
-However, if you haven't managed to get redis running on your computer, you
-can change `CELERY_TASK_ALWAYS_EAGER` to `true`. This will disable the task
-queue, and cause all Celery tasks to be run immediately rather than queuing
-them for later.
-
-## Set Up HTTPS
-
-Nylas requires that all webhooks be delivered to the secure HTTPS endpoints,
-rather than insecure HTTP endpoints. There are several ways
-to set up HTTPS on your computer, but perhaps the simplest is to use
-[ngrok](https://ngrok.com), a tool that lets you create a secure tunnel
-from the ngrok website to your computer. Install it from the website, and
-then run the following command:
-
-```
-ngrok http 5000
-```
-
-Notice that ngrok will show you two "forwarding" URLs, which may look something
-like `http://ed90abe7.ngrok.io` and `https://ed90abe7.ngrok.io`. (The hash
-subdomain will be different for you.) You'll be using the second URL, which
-starts with `https`.
-
-## Install the Dependencies
-
-This project depends on a few third-party Python modules.
-These dependencies are listed in the `requirements.txt` file in this directory.
-To install them, use the `pip` tool, like this:
-
-```
-pip install -r requirements.txt
-```
-
-## Run the Celery worker (if you're using redis)
-
-The Celery worker will continuously check the task queue to see if there are
-any new tasks to be run, and it will run any tasks that it finds. Without at
-least one worker running, tasks on the task queue will sit there unfinished
-forever. To run a celery worker, pass the `--worker` argument to the `server.py`
-script, like this:
-
-```
-python server.py --worker
-```
-
-Note that if you're not using redis, you don't need to run a Celery worker,
-because the tasks will be run immediately rather than put on the task queue.
-
-## Run the Example
-
-While the Celery worker is running, open a new terminal window and run the
-Flask web server, like this:
-
-```
-python server.py
-```
-
-You should see the ngrok URL in the console, and the web server will start
-on port 5000.
-
-## Set the Nylas Callback URL
-
-Now that your webhook is all set up and running, you need to tell
-Nylas about it. On the [Nylas Developer](https://developer.nylas.com) console,
-click on the "Webhooks" tab on the left side, then click the "Add Webhook"
-button.
-Paste your HTTPS URL into text field, and add `/webhook`
-after it. For example, if your HTTPS URL is `https://ad172180.ngrok.io`, then
-you would put `https://ad172180.ngrok.io/webhook` into the text field.
-
-Then click the "Create Webhook" button to save.
-
-## Trigger events and see webhook notifications!
-
-Send an email on an account that's connected to Nylas. In a minute or two,
-you'll get a webhook notification with information about the event that just
-happened!
-
-If you're using redis, you should see the information about the event in the
-terminal window where your Celery worker is running. If you're not using
-redis, you should see the information about the event in the terminal window
-where your Flask web server is running.
diff --git a/examples/webhooks/config.json b/examples/webhooks/config.json
deleted file mode 100644
index 04e9a645..00000000
--- a/examples/webhooks/config.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "SECRET_KEY": "replace me with a random string",
- "NYLAS_OAUTH_CLIENT_ID": "replace me with the client ID from Nylas",
- "NYLAS_OAUTH_CLIENT_SECRET": "replace me with the client secret from Nylas",
- "CELERY_BROKER_URL": "redis://localhost",
- "CELERY_TASK_ALWAYS_EAGER": false
-}
diff --git a/examples/webhooks/requirements.txt b/examples/webhooks/requirements.txt
deleted file mode 100644
index 38e90172..00000000
--- a/examples/webhooks/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Flask>=0.11
-celery[redis]>=4.0.0
-requests
diff --git a/examples/webhooks/server.py b/examples/webhooks/server.py
deleted file mode 100755
index db1deb95..00000000
--- a/examples/webhooks/server.py
+++ /dev/null
@@ -1,201 +0,0 @@
-#!/usr/bin/env python
-
-# Imports from the Python standard library
-from __future__ import print_function
-import os
-import sys
-import datetime
-import textwrap
-import hmac
-import hashlib
-
-# Imports from third-party modules that this project depends on
-try:
- import requests
- from flask import Flask, request, render_template
- from werkzeug.middleware.proxy_fix import ProxyFix
- from celery import Celery
-except ImportError:
- message = textwrap.dedent(
- """
- You need to install the dependencies for this project.
- To do so, run this command:
-
- pip install -r requirements.txt
- """
- )
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# This example uses Flask, a micro web framework written in Python.
-# For more information, check out the documentation: http://flask.pocoo.org
-# Create a Flask app, and load the configuration file.
-app = Flask(__name__)
-app.config.from_json("config.json")
-
-# Check for dummy configuration values.
-# If you are building your own application based on this example,
-# you can remove this check from your code.
-cfg_needs_replacing = [
- key
- for key, value in app.config.items()
- if isinstance(value, str) and value.startswith("replace me")
-]
-if cfg_needs_replacing:
- message = textwrap.dedent(
- """
- This example will only work if you replace the fake configuration
- values in `config.json` with real configuration values.
- The following config values need to be replaced:
- {keys}
- Consult the README.md file in this directory for more information.
- """
- ).format(keys=", ".join(cfg_needs_replacing))
- print(message, file=sys.stderr)
- sys.exit(1)
-
-# Teach Flask how to find out that it's behind an ngrok proxy
-app.wsgi_app = ProxyFix(app.wsgi_app)
-
-# This example also uses Celery, a task queue framework written in Python.
-# For more information, check out the documentation: http://docs.celeryproject.org
-# Create a Celery instance, and load its configuration from Flask.
-celery = Celery(app.import_name)
-celery.config_from_object(app.config, namespace="CELERY")
-
-
-@app.route("/webhook", methods=["GET", "POST"])
-def webhook():
- """
- When the Flask server gets a request at the `/webhook` URL, it will run
- this function. Most of the time, that request will be a genuine webhook
- notification from Nylas. However, it's possible that the request could
- be a fake notification from someone else, trying to fool our app. This
- function needs to verify that the webhook is genuine!
- """
- # When you first tell Nylas about your webhook, it will test that webhook
- # URL with a GET request to make sure that it responds correctly.
- # We just need to return the `challenge` parameter to indicate that this
- # is a valid webhook URL.
- if request.method == "GET" and "challenge" in request.args:
- print(" * Nylas connected to the webhook!")
- return request.args["challenge"]
-
- # Alright, this is a POST request, which means it's a webhook notification.
- # The question is, is it genuine or fake? Check the signature to find out.
- is_genuine = verify_signature(
- message=request.data,
- key=app.config["NYLAS_OAUTH_CLIENT_SECRET"].encode("utf8"),
- signature=request.headers.get("X-Nylas-Signature"),
- )
- if not is_genuine:
- return "Signature verification failed!", 401
-
- # Alright, we have a genuine webhook notification from Nylas!
- # Let's find out what it says...
- data = request.get_json()
- for delta in data["deltas"]:
- # Processing the data might take awhile, or it might fail.
- # As a result, instead of processing it right now, we'll push a task
- # onto the Celery task queue, to handle it later. That way,
- # we've got the data saved, and we can return a response to the
- # Nylas webhook notification right now.
- process_delta.delay(delta)
-
- # Now that all the `process_delta` tasks have been queued, we can
- # return an HTTP response to Nylas, to let them know that we processed
- # the webhook notification successfully.
- return "Deltas have been queued", 200
-
-
-def verify_signature(message, key, signature):
- """
- This function will verify the authenticity of a digital signature.
- For security purposes, Nylas includes a digital signature in the headers
- of every webhook notification, so that clients can verify that the
- webhook request came from Nylas and no one else. The signing key
- is your OAuth client secret, which only you and Nylas know.
- """
- digest = hmac.new(key, msg=message, digestmod=hashlib.sha256).hexdigest()
- return hmac.compare_digest(digest, signature)
-
-
-@celery.task
-def process_delta(delta):
- """
- This is the part of the code where you would process the information
- from the webhook notification. Each delta is one change that happened,
- and might require fetching message IDs, updating your database,
- and so on.
-
- However, because this is just an example project, we'll just print
- out information about the notification, so you can see what
- information is being sent.
- """
- kwargs = {
- "type": delta["type"],
- "date": datetime.datetime.utcfromtimestamp(delta["date"]),
- "object_id": delta["object_data"]["id"],
- }
- print(" * {type} at {date} with ID {object_id}".format(**kwargs))
-
-
-@app.route("/")
-def index():
- """
- This makes sure that when you visit the root of the website,
- you get a webpage rather than a 404 error.
- """
- return render_template("index.html", ngrok_url=ngrok_url())
-
-
-def ngrok_url():
- """
- If ngrok is running, it exposes an API on port 4040. We can use that
- to figure out what URL it has assigned, and suggest that to the user.
- https://ngrok.com/docs#list-tunnels
- """
- try:
- ngrok_resp = requests.get("http://localhost:4040/api/tunnels")
- except requests.ConnectionError:
- # I guess ngrok isn't running.
- return None
- ngrok_data = ngrok_resp.json()
- secure_urls = [
- tunnel["public_url"]
- for tunnel in ngrok_data["tunnels"]
- if tunnel["proto"] == "https"
- ]
- return secure_urls[0]
-
-
-# When this file is executed, this block of code will run.
-if __name__ == "__main__":
- if len(sys.argv) > 1 and sys.argv[1] == "--worker":
- # Run the celery worker, *instead* of running the Flask web server.
- celery.worker_main(sys.argv[1:])
- sys.exit()
-
- # If we get here, we're going to try to run the Flask web server.
- url = ngrok_url()
- if not url:
- print(
- "Looks like ngrok isn't running! Start it by running "
- "`ngrok http 5000` in a different terminal window, "
- "and then try running this example again.",
- file=sys.stderr,
- )
- sys.exit(1)
-
- print(" * Webhook URL: {url}/webhook".format(url=url))
-
- if app.config.get("CELERY_TASK_ALWAYS_EAGER"):
- print(" * Celery tasks will be run synchronously. No worker needed.")
- elif len(celery.control.inspect().stats().keys()) < 2:
- print(
- " * You need to run at least one Celery worker, otherwise "
- "the webhook notifications will never be processed.\n"
- " To do so, run `{arg0} --worker` in a different "
- "terminal window.".format(arg0=sys.argv[0])
- )
- app.run()
diff --git a/examples/webhooks/templates/base.html b/examples/webhooks/templates/base.html
deleted file mode 100644
index 3d6ac8a3..00000000
--- a/examples/webhooks/templates/base.html
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- Nylas Webhook Example
-
-
-
-
This example doesn't have anything to see in the browser. Set up your
- webhook on the
- Nylas Developer console,
- and then watch your terminal to see the webhook notifications come in.
-
-
-
Your webhook URL is:
- {{ ngrok_url }}{{ url_for("webhook") }}
-
-
-
Once you've received at least one webhook notification from Nylas,
- you might want to check out the
- ngrok web interface.
- That will allow you to see more information about the webhook notification,
- and replay it for testing purposes if you want.
-
-{% endblock %}
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..6e8dfce4
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,23 @@
+site_name: Nylas Python SDK Reference
+theme:
+ name: 'material'
+
+nav:
+ - Getting Started: index.md
+ - Code Reference: reference/
+ - Contributing: contributing.md
+ - License: license.md
+
+# Add plugins
+plugins:
+ - search
+ - mkdocstrings:
+ default_handler: python
+ handlers:
+ python:
+ paths: [ nylas ]
+ - gen-files:
+ scripts:
+ - scripts/generate-docs.py
+ - literate-nav:
+ nav_file: SUMMARY.md
diff --git a/nylas/__init__.py b/nylas/__init__.py
index da9b836f..2befcf83 100644
--- a/nylas/__init__.py
+++ b/nylas/__init__.py
@@ -1,6 +1,3 @@
-from pkgutil import extend_path
-from .client.client import APIClient
+from nylas.client import Client
-# Allow out-of-tree submodules.
-__path__ = extend_path(__path__, __name__)
-__all__ = ["APIClient"]
+__all__ = ["Client"]
diff --git a/nylas/_client_sdk_version.py b/nylas/_client_sdk_version.py
index cd742bfa..85e889a5 100644
--- a/nylas/_client_sdk_version.py
+++ b/nylas/_client_sdk_version.py
@@ -1 +1 @@
-__VERSION__ = "5.2.0"
+__VERSION__ = "6.15.0"
diff --git a/nylas/client.py b/nylas/client.py
new file mode 100644
index 00000000..66ce84b1
--- /dev/null
+++ b/nylas/client.py
@@ -0,0 +1,248 @@
+from nylas.config import DEFAULT_SERVER_URL
+from nylas.handler.http_client import HttpClient
+from nylas.resources.applications import Applications
+from nylas.resources.attachments import Attachments
+from nylas.resources.auth import Auth
+from nylas.resources.calendars import Calendars
+from nylas.resources.connectors import Connectors
+from nylas.resources.events import Events
+from nylas.resources.folders import Folders
+from nylas.resources.messages import Messages
+from nylas.resources.lists import Lists
+from nylas.resources.threads import Threads
+from nylas.resources.transactional_send import TransactionalSend
+from nylas.resources.webhooks import Webhooks
+from nylas.resources.contacts import Contacts
+from nylas.resources.drafts import Drafts
+from nylas.resources.domains import Domains
+from nylas.resources.grants import Grants
+from nylas.resources.policies import Policies
+from nylas.resources.scheduler import Scheduler
+from nylas.resources.notetakers import Notetakers
+from nylas.resources.rules import Rules
+
+
+class Client:
+ """
+ API client for the Nylas API.
+
+ Attributes:
+ api_key: The Nylas API key to use for authentication
+ api_uri: The URL to use for communicating with the Nylas API
+ http_client: The HTTP client to use for requests to the Nylas API
+ """
+
+ def __init__(
+ self, api_key: str, api_uri: str = DEFAULT_SERVER_URL, timeout: int = 90
+ ):
+ """
+ Initialize the Nylas API client.
+
+ Args:
+ api_key: The Nylas API key to use for authentication
+ api_uri: The URL to use for communicating with the Nylas API
+ timeout: The timeout for requests to the Nylas API, in seconds
+ """
+ self.api_key = api_key
+ self.api_uri = api_uri
+ self.http_client = HttpClient(self.api_uri, self.api_key, timeout)
+
+ @property
+ def auth(self) -> Auth:
+ """
+ Access the Auth API.
+
+ Returns:
+ The Auth API.
+ """
+ return Auth(self.http_client)
+
+ @property
+ def applications(self) -> Applications:
+ """
+ Access the Applications API.
+
+ Returns:
+ The Applications API.
+ """
+ return Applications(self.http_client)
+
+ @property
+ def attachments(self) -> Attachments:
+ """
+ Access the Attachments API.
+
+ Returns:
+ The Attachments API.
+ """
+ return Attachments(self.http_client)
+
+ @property
+ def connectors(self) -> Connectors:
+ """
+ Access the Connectors API.
+
+ Returns:
+ The Connectors API.
+ """
+ return Connectors(self.http_client)
+
+ @property
+ def calendars(self) -> Calendars:
+ """
+ Access the Calendars API.
+
+ Returns:
+ The Calendars API.
+ """
+ return Calendars(self.http_client)
+
+ @property
+ def contacts(self) -> Contacts:
+ """
+ Access the Contacts API.
+
+ Returns:
+ The Contacts API.
+ """
+ return Contacts(self.http_client)
+
+ @property
+ def drafts(self) -> Drafts:
+ """
+ Access the Drafts API.
+
+ Returns:
+ The Drafts API.
+ """
+ return Drafts(self.http_client)
+
+ @property
+ def domains(self) -> Domains:
+ """
+ Access the Manage Domains API.
+
+ Returns:
+ The Manage Domains API.
+ """
+ return Domains(self.http_client)
+
+ @property
+ def events(self) -> Events:
+ """
+ Access the Events API.
+
+ Returns:
+ The Events API.
+ """
+ return Events(self.http_client)
+
+ @property
+ def folders(self) -> Folders:
+ """
+ Access the Folders API.
+
+ Returns:
+ The Folders API.
+ """
+ return Folders(self.http_client)
+
+ @property
+ def grants(self) -> Grants:
+ """
+ Access the Grants API.
+
+ Returns:
+ The Grants API.
+ """
+ return Grants(self.http_client)
+
+ @property
+ def policies(self) -> Policies:
+ """
+ Access the Policies API.
+
+ Returns:
+ The Policies API.
+ """
+ return Policies(self.http_client)
+
+ @property
+ def rules(self) -> Rules:
+ """
+ Access the Rules API.
+
+ Returns:
+ The Rules API.
+ """
+ return Rules(self.http_client)
+
+ @property
+ def messages(self) -> Messages:
+ """
+ Access the Messages API.
+
+ Returns:
+ The Messages API.
+ """
+ return Messages(self.http_client)
+
+ @property
+ def lists(self) -> Lists:
+ """
+ Access the Lists API.
+
+ Returns:
+ The Lists API.
+ """
+ return Lists(self.http_client)
+
+ @property
+ def threads(self) -> Threads:
+ """
+ Access the Threads API.
+
+ Returns:
+ The Threads API.
+ """
+ return Threads(self.http_client)
+
+ @property
+ def transactional_send(self) -> TransactionalSend:
+ """
+ Access the Transactional Send API.
+
+ Returns:
+ The Transactional Send API.
+ """
+ return TransactionalSend(self.http_client)
+
+ @property
+ def webhooks(self) -> Webhooks:
+ """
+ Access the Webhooks API.
+
+ Returns:
+ The Webhooks API.
+ """
+ return Webhooks(self.http_client)
+
+ @property
+ def scheduler(self) -> Scheduler:
+ """
+ Access the Scheduler API.
+
+ Returns:
+ The Scheduler API.
+ """
+ return Scheduler(self.http_client)
+
+ @property
+ def notetakers(self) -> Notetakers:
+ """
+ Access the Notetakers API.
+
+ Returns:
+ The Notetakers API.
+ """
+ return Notetakers(self.http_client)
diff --git a/nylas/client/__init__.py b/nylas/client/__init__.py
deleted file mode 100644
index 57d8f800..00000000
--- a/nylas/client/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from pkgutil import extend_path
-from .client import APIClient
-
-__path__ = extend_path(__path__, __name__)
-__all__ = ["APIClient"]
diff --git a/nylas/client/client.py b/nylas/client/client.py
deleted file mode 100644
index d7d68065..00000000
--- a/nylas/client/client.py
+++ /dev/null
@@ -1,664 +0,0 @@
-from __future__ import print_function
-import sys
-from os import environ
-from base64 import b64encode
-import json
-from datetime import datetime, timedelta
-from itertools import chain
-
-import requests
-from requests import HTTPError
-from urlobject import URLObject
-import six
-from six.moves.urllib.parse import urlencode
-from nylas._client_sdk_version import __VERSION__
-from nylas.client.errors import MessageRejectedError, NylasApiError
-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,
-)
-from nylas.client.neural_api_models import Neural
-from nylas.utils import timestamp_from_dt, create_request_body
-
-DEBUG = environ.get("NYLAS_CLIENT_DEBUG")
-API_SERVER = "https://api.nylas.com"
-SUPPORTED_API_VERSION = "2.2"
-
-
-def _validate(response):
- if DEBUG: # pragma: no cover
- print(
- "{method} {url} ({body}) => {status}: {text}".format(
- method=response.request.method,
- url=response.request.url,
- body=response.request.body,
- status=response.status_code,
- text=response.text,
- )
- )
-
- if response.status_code == 402:
- # HTTP status code 402 normally means "Payment Required",
- # but when Nylas uses that status code, it means something different.
- # Usually it indicates an upstream error on the provider.
- # We let Requests handle most HTTP errors, but for this one,
- # we will handle it separate and handle a _different_ exception
- # so that users don't think they need to pay.
- raise MessageRejectedError(response)
- elif response.status_code >= 400:
- raise NylasApiError(response)
-
- return response
-
-
-class APIClient(json.JSONEncoder):
- """API client for the Nylas API."""
-
- def __init__(
- self,
- client_id=environ.get("NYLAS_CLIENT_ID"),
- client_secret=environ.get("NYLAS_CLIENT_SECRET"),
- access_token=environ.get("NYLAS_ACCESS_TOKEN"),
- api_server=API_SERVER,
- api_version=SUPPORTED_API_VERSION,
- ):
- if not api_server.startswith("https://"):
- raise Exception(
- "When overriding the Nylas API server address, you"
- " must include https://"
- )
- self.api_server = api_server
- self.api_version = api_version
- self.authorize_url = api_server + "/oauth/authorize"
- self.access_token_url = api_server + "/oauth/token"
- self.revoke_url = api_server + "/oauth/revoke"
- self.revoke_all_url = (
- api_server + "/a/{client_id}/accounts/{account_id}/revoke-all"
- )
- self.ip_addresses_url = api_server + "/a/{client_id}/ip_addresses"
- self.token_info_url = (
- api_server + "/a/{client_id}/accounts/{account_id}/token-info"
- )
-
- self.client_secret = client_secret
- self.client_id = client_id
-
- self.session = requests.Session()
- self.version = __VERSION__
- major, minor, revision, _, __ = sys.version_info
- version_header = "Nylas Python SDK {} - {}.{}.{}".format(
- self.version, major, minor, revision
- )
- self.session.headers = {
- "X-Nylas-API-Wrapper": "python",
- "X-Nylas-Client-Id": self.client_id,
- "Nylas-API-Version": self.api_version,
- "User-Agent": version_header,
- }
- self._access_token = None
- self.access_token = access_token
- self.auth_token = None
-
- # Requests to the /a/ namespace don't use an auth token but
- # the client_secret. Set up a specific session for this.
- self.admin_session = requests.Session()
-
- if client_secret is not None:
- b64_client_secret = b64encode((client_secret + ":").encode("utf8"))
- authorization = "Basic {secret}".format(
- secret=b64_client_secret.decode("utf8")
- )
- self.admin_session.headers = {
- "Authorization": authorization,
- "X-Nylas-API-Wrapper": "python",
- "X-Nylas-Client-Id": self.client_id,
- "Nylas-API-Version": self.api_version,
- "User-Agent": version_header,
- }
- super(APIClient, self).__init__()
-
- @property
- def access_token(self):
- return self._access_token
-
- @access_token.setter
- def access_token(self, value):
- self._access_token = value
- if value:
- authorization = "Bearer {token}".format(token=value)
- self.session.headers["Authorization"] = authorization
- else:
- if "Authorization" in self.session.headers:
- del self.session.headers["Authorization"]
-
- def authentication_url(
- self,
- redirect_uri,
- login_hint="",
- state="",
- scopes=("email", "calendar", "contacts"),
- ):
- args = {
- "redirect_uri": redirect_uri,
- "client_id": self.client_id or "None", # 'None' for back-compat
- "response_type": "code",
- "login_hint": login_hint,
- "state": state,
- }
-
- if scopes:
- if isinstance(scopes, str):
- scopes = [scopes]
- args["scopes"] = ",".join(scopes)
-
- url = URLObject(self.authorize_url).add_query_params(args.items())
- return str(url)
-
- def token_for_code(self, code):
- args = {
- "client_id": self.client_id,
- "client_secret": self.client_secret,
- "grant_type": "authorization_code",
- "code": code,
- }
-
- headers = {
- "Content-type": "application/x-www-form-urlencoded",
- "Accept": "text/plain",
- }
-
- resp = requests.post(
- self.access_token_url, data=urlencode(args), headers=headers
- ).json()
-
- self.access_token = resp[u"access_token"]
- return self.access_token
-
- def is_opensource_api(self):
- if self.client_id is None and self.client_secret is None:
- return True
-
- return False
-
- def revoke_token(self):
- resp = self.session.post(self.revoke_url)
- _validate(resp)
- self.auth_token = None
- self.access_token = None
-
- def revoke_all_tokens(self, keep_access_token=None):
- revoke_all_url = self.revoke_all_url.format(
- client_id=self.client_id, account_id=self.account.id
- )
- data = {}
- if keep_access_token is not None:
- data["keep_access_token"] = keep_access_token
-
- headers = {"Content-Type": "application/json"}
- headers.update(self.admin_session.headers)
- resp = self.admin_session.post(revoke_all_url, json=data, headers=headers)
- _validate(resp).json()
- if keep_access_token != self.access_token:
- self.auth_token = None
- self.access_token = None
-
- def ip_addresses(self):
- ip_addresses_url = self.ip_addresses_url.format(client_id=self.client_id)
- resp = self.admin_session.get(ip_addresses_url)
- _validate(resp).json()
- return resp.json()
-
- def token_info(self):
- token_info_url = self.token_info_url.format(
- client_id=self.client_id, account_id=self.account.id
- )
- self.admin_session.headers["Content-Type"] = "application/json"
- resp = self.admin_session.post(
- token_info_url, json={"access_token": self.access_token}
- )
- _validate(resp).json()
- return resp.json()
-
- def free_busy(self, emails, start_at, end_at):
- if isinstance(emails, six.string_types):
- emails = [emails]
- 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
- url = "{api_server}/calendars/free-busy".format(api_server=self.api_server)
- data = {
- "emails": emails,
- "start_time": start_time,
- "end_time": end_time,
- }
- resp = self.session.post(url, json=data)
- _validate(resp)
- return resp.json()
-
- def open_hours(self, emails, days, timezone, start, end):
- if isinstance(emails, six.string_types):
- emails = [emails]
- if isinstance(days, int):
- days = [days]
- if isinstance(start, datetime):
- start = "{hour}:{minute}".format(hour=start.hour, minute=start.minute)
- if isinstance(start, datetime):
- end = "{hour}:{minute}".format(hour=end.hour, minute=end.minute)
- return {
- "emails": emails,
- "days": days,
- "timezone": timezone,
- "start": start,
- "end": end,
- "object_type": "open_hours",
- }
-
- def availability(
- self,
- emails,
- duration,
- interval,
- start_at,
- end_at,
- buffer=None,
- round_robin=None,
- free_busy=None,
- open_hours=None,
- ):
- if isinstance(emails, six.string_types):
- emails = [emails]
- 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".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,
- "buffer": buffer,
- "round_robin": round_robin,
- "free_busy": free_busy or [],
- "open_hours": open_hours or [],
- }
- resp = self.session.post(url, json=data)
- _validate(resp)
- return resp.json()
-
- def consecutive_availability(
- 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,
- "buffer": buffer,
- "free_busy": free_busy or [],
- "open_hours": open_hours or [],
- }
- 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 components(self):
- return RestfulModelCollection(Component, self)
-
- @property
- def neural(self):
- return Neural(self)
-
- ##########################################################
- # Private functions used by Restful Model Collection #
- ##########################################################
-
- def _get_http_session(self, api_root):
- # Is this a request for a resource under the accounts/billing/admin
- # namespace (/a)? If the latter, pass the client_secret
- # instead of the secret_token
- if api_root:
- return self.admin_session
- return self.session
-
- def _get_resources(self, cls, extra=None, **filters):
- # FIXME @karim: remove this interim code when we've got rid
- # of the old accounts API.
- postfix = "/{}".format(extra) if extra else ""
- path = "/{}".format(cls.collection_name) if cls.collection_name else ""
- if not cls.api_root:
- url = "{server}{path}{postfix}".format(
- server=self.api_server, path=path, postfix=postfix
- )
- else:
- url = "{server}/{prefix}/{client_id}{path}{postfix}".format(
- server=self.api_server,
- prefix=cls.api_root,
- client_id=self.client_id,
- path=path,
- postfix=postfix,
- )
-
- converted_data = create_request_body(filters, cls.datetime_filter_attrs)
- url = str(URLObject(url).add_query_params(converted_data.items()))
- response = self._get_http_session(cls.api_root).get(url)
- results = _validate(response).json()
- return [cls.create(self, **x) for x in results if x is not None]
-
- def _get_resource_raw(
- self, cls, id, extra=None, headers=None, stream=False, **filters
- ):
- """Get an individual REST resource"""
- postfix = "/{}".format(extra) if extra else ""
- path = "/{}".format(cls.collection_name) if cls.collection_name else ""
- id = "/{}".format(id) if id else ""
- if not cls.api_root:
- url = "{server}{path}{id}{postfix}".format(
- server=self.api_server, path=path, id=id, postfix=postfix
- )
- else:
- url = "{server}/{prefix}/{client_id}{path}{id}{postfix}".format(
- server=self.api_server,
- prefix=cls.api_root,
- client_id=self.client_id,
- path=path,
- id=id,
- postfix=postfix,
- )
-
- converted_data = create_request_body(filters, cls.datetime_filter_attrs)
- url = str(URLObject(url).add_query_params(converted_data.items()))
-
- session = self._get_http_session(cls.api_root)
-
- headers = headers or {}
- headers.update(session.headers)
- response = session.get(url, headers=headers, stream=stream)
- return _validate(response)
-
- def _get_resource(self, cls, id, **filters):
- response = self._get_resource_raw(cls, id, **filters)
- result = response.json()
- if isinstance(result, list):
- result = result[0]
- return cls.create(self, **result)
-
- def _get_resource_data(self, cls, id, extra=None, headers=None, **filters):
- response = self._get_resource_raw(
- cls, id, extra=extra, headers=headers, **filters
- )
- return response.content
-
- def _create_resource(self, cls, data, **kwargs):
- name = "{prefix}{path}".format(
- prefix="/{}/{}".format(cls.api_root, self.client_id)
- if cls.api_root
- else "",
- path="/{}".format(cls.collection_name) if cls.collection_name else "",
- )
- url = (
- URLObject(self.api_server)
- .with_path("{name}".format(name=name))
- .set_query_params(**kwargs)
- )
-
- session = self._get_http_session(cls.api_root)
-
- if cls == File:
- response = session.post(url, files=data)
- else:
- converted_data = create_request_body(data, cls.datetime_attrs)
- headers = {"Content-Type": "application/json"}
- headers.update(session.headers)
- response = session.post(url, json=converted_data, headers=headers)
-
- result = _validate(response).json()
- if cls.collection_name == "send":
- return result
- return cls.create(self, **result)
-
- def _create_resources(self, cls, data):
- name = "{prefix}{path}".format(
- prefix="/{}/{}".format(cls.api_root, self.client_id)
- if cls.api_root
- else "",
- path="/{}".format(cls.collection_name) if cls.collection_name else "",
- )
- url = URLObject(self.api_server).with_path("{name}".format(name=name))
- session = self._get_http_session(cls.api_root)
-
- if cls == File:
- response = session.post(url, files=data)
- else:
- converted_data = [
- create_request_body(datum, cls.datetime_attrs) for datum in data
- ]
- headers = {"Content-Type": "application/json"}
- headers.update(session.headers)
- response = session.post(url, json=converted_data, headers=headers)
-
- results = _validate(response).json()
- return [cls.create(self, **x) for x in results]
-
- def _delete_resource(self, cls, id, data=None, **kwargs):
- name = "{prefix}{path}".format(
- prefix="/{}/{}".format(cls.api_root, self.client_id)
- if cls.api_root
- else "",
- path="/{}".format(cls.collection_name) if cls.collection_name else "",
- )
- url = (
- URLObject(self.api_server)
- .with_path("{name}/{id}".format(name=name, id=id))
- .set_query_params(**kwargs)
- )
- session = self._get_http_session(cls.api_root)
- if data:
- _validate(session.delete(url, json=data))
- else:
- _validate(session.delete(url))
-
- def _update_resource(self, cls, id, data, **kwargs):
- name = "{prefix}{path}".format(
- prefix="/{}/{}".format(cls.api_root, self.client_id)
- if cls.api_root
- else "",
- path="/{}".format(cls.collection_name) if cls.collection_name else "",
- )
- url = (
- URLObject(self.api_server)
- .with_path("{name}/{id}".format(name=name, id=id))
- .set_query_params(**kwargs)
- )
-
- session = self._get_http_session(cls.api_root)
-
- converted_data = create_request_body(data, cls.datetime_attrs)
- response = session.put(url, json=converted_data)
-
- result = _validate(response).json()
- return cls.create(self, **result)
-
- def _call_resource_method(self, cls, id, method_name, data):
- """POST a dictionary to an API method,
- for example /a/.../accounts/id/upgrade"""
-
- path = "/{}".format(cls.collection_name) if cls.collection_name else ""
- if not cls.api_root:
- url_path = "/{name}/{id}/{method}".format(
- name=cls.collection_name, id=id, method=method_name
- )
- else:
- # Management method.
- url_path = "/{prefix}/{client_id}{path}/{id}/{method}".format(
- prefix=cls.api_root,
- client_id=self.client_id,
- path=path,
- id=id,
- method=method_name,
- )
-
- url = URLObject(self.api_server).with_path(url_path)
- converted_data = create_request_body(data, cls.datetime_attrs)
-
- session = self._get_http_session(cls.api_root)
- response = session.post(url, json=converted_data)
-
- result = _validate(response).json()
- return cls.create(self, **result)
-
- def _request_neural_resource(self, cls, data, path=None, method="PUT"):
- if path is None:
- path = cls.collection_name
- url = URLObject(self.api_server).with_path("/neural/{name}".format(name=path))
-
- session = self._get_http_session(cls.api_root)
-
- converted_data = create_request_body(data, cls.datetime_attrs)
- response = session.request(method, url, json=converted_data)
-
- result = _validate(response).json()
- if isinstance(result, list):
- object_list = []
- for obj in result:
- object_list.append(cls.create(self, **obj))
- return object_list
-
- return cls.create(self, **result)
-
- def _validate_open_hours(self, emails, open_hours, free_busy):
- if isinstance(open_hours, list) is False:
- raise ValueError("'open_hours' must be an array.")
- open_hours_emails = list(
- chain.from_iterable([oh["emails"] for oh in open_hours])
- )
- free_busy_emails = (
- [fb["email"] for fb in free_busy] if free_busy is not None else []
- )
- if isinstance(emails[0], list) is True:
- emails = list(chain.from_iterable(emails))
- for email in open_hours_emails:
- if (email in emails) is False and (email in free_busy_emails) is False:
- raise ValueError(
- "Open Hours cannot contain an email not present in the main email list or the free busy email list."
- )
diff --git a/nylas/client/errors.py b/nylas/client/errors.py
deleted file mode 100644
index 49448116..00000000
--- a/nylas/client/errors.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import json
-
-from requests import HTTPError
-
-
-class NylasError(Exception):
- pass
-
-
-class MessageRejectedError(NylasError):
- pass
-
-
-class FileUploadError(NylasError):
- pass
-
-
-class UnSyncedError(NylasError):
- """
- HTTP Code 202
- The request was valid but the resource wasn't ready. Retry the request with exponential backoff.
- """
-
- pass
-
-
-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 = u"%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)
diff --git a/nylas/client/neural_api_models.py b/nylas/client/neural_api_models.py
deleted file mode 100644
index 0911ef46..00000000
--- a/nylas/client/neural_api_models.py
+++ /dev/null
@@ -1,183 +0,0 @@
-from nylas.client.restful_models import RestfulModel, Message, File, Contact
-import re
-
-
-def _add_options_to_body(body, options):
- options_dict = options.__dict__
- # Only append set options to body to prevent a 400 error
- options_filtered = {k: v for k, v in options_dict.items() if v is not None}
- return body.update(options_filtered)
-
-
-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)
-
-
-class NeuralMessageOptions:
- def __init__(
- self,
- ignore_links=None,
- ignore_images=None,
- ignore_tables=None,
- remove_conclusion_phrases=None,
- images_as_markdowns=None,
- ):
- self.ignore_links = ignore_links
- self.ignore_images = ignore_images
- self.ignore_tables = ignore_tables
- self.remove_conclusion_phrases = remove_conclusion_phrases
- self.images_as_markdowns = images_as_markdowns
-
-
-class NeuralSentimentAnalysis(RestfulModel):
- attrs = [
- "account_id",
- "sentiment",
- "sentiment_score",
- "processed_length",
- "text",
- ]
- collection_name = "sentiment"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralSentimentAnalysis, api)
-
-
-class NeuralSignatureExtraction(Message):
- attrs = Message.attrs + ["signature", "model_version", "contacts"]
- collection_name = "signature"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralSignatureExtraction, api)
-
-
-class NeuralSignatureContact(RestfulModel):
- attrs = ["job_titles", "links", "phone_numbers", "emails", "names"]
- collection_name = "signature_contact"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralSignatureContact, api)
-
- def to_contact_object(self):
- contact = {}
- if self.names is not None:
- contact["given_name"] = self.names[0]["first_name"]
- contact["surname"] = self.names[0]["last_name"]
- if self.job_titles is not None:
- contact["job_title"] = self.job_titles[0]
- if self.emails is not None:
- contact["emails"] = []
- for email in self.emails:
- contact["emails"].append({"type": "personal", "email": email})
- if self.phone_numbers is not None:
- contact["phone_numbers"] = []
- for number in self.phone_numbers:
- contact["phone_numbers"].append({"type": "mobile", "number": number})
- if self.links is not None:
- contact["web_pages"] = []
- for url in self.links:
- description = url["description"] if url["description"] else "homepage"
- contact["web_pages"].append({"type": description, "url": url["url"]})
-
- return Contact.create(self.api, **contact)
-
-
-class NeuralCategorizer(Message):
- attrs = Message.attrs + ["categorizer"]
- collection_name = "categorize"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralCategorizer, api)
-
- def recategorize(self, category):
- data = {"message_id": self.id, "category": category}
- self.api._request_neural_resource(
- NeuralCategorizer, data, "categorize/feedback", "POST"
- )
- data = {"message_id": self.id}
- response = self.api._request_neural_resource(NeuralCategorizer, data)
- categorize = response[0]
- if categorize.categorizer:
- categorize.categorizer = Categorize.create(
- self.api, **categorize.categorizer
- )
- return categorize
-
-
-class Categorize(RestfulModel):
- attrs = ["category", "categorized_at", "model_version", "subcategories"]
- datetime_attrs = {"categorized_at": "categorized_at"}
- collection_name = "category"
-
- def __init__(self, api):
- RestfulModel.__init__(self, Categorize, api)
-
-
-class NeuralCleanConversation(Message):
- attrs = Message.attrs + [
- "conversation",
- "model_version",
- ]
- collection_name = "conversation"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralCleanConversation, api)
-
- def extract_images(self):
- pattern = "[\(']cid:(.*?)[\)']"
- file_ids = re.findall(pattern, self.conversation)
- files = []
- for match in file_ids:
- files.append(self.api.files.get(match))
- return files
-
-
-class NeuralOcr(File):
- attrs = File.attrs + [
- "ocr",
- "processed_pages",
- ]
- collection_name = "ocr"
-
- def __init__(self, api):
- RestfulModel.__init__(self, NeuralOcr, api)
diff --git a/nylas/client/restful_model_collection.py b/nylas/client/restful_model_collection.py
deleted file mode 100644
index 596c2a68..00000000
--- a/nylas/client/restful_model_collection.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from copy import copy
-from nylas.utils import convert_metadata_pairs_to_array
-
-CHUNK_SIZE = 50
-
-
-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
diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py
deleted file mode 100644
index 9e53382e..00000000
--- a/nylas/client/restful_models.py
+++ /dev/null
@@ -1,822 +0,0 @@
-from datetime import datetime
-from collections import defaultdict
-
-from six import StringIO
-from nylas.client.restful_model_collection import RestfulModelCollection
-from nylas.client.errors import FileUploadError, UnSyncedError, NylasApiError
-from nylas.utils import timestamp_from_dt
-
-# pylint: disable=attribute-defined-outside-init
-
-
-def typed_dict_attr(items, attr_name=None):
- if attr_name:
- pairs = [(item["type"], item[attr_name]) for item in items]
- else:
- pairs = [(item["type"], item) for item in items]
- dct = defaultdict(list)
- for key, value in pairs:
- dct[key].append(value)
- return dct
-
-
-def _is_subclass(cls, parent):
- for base in cls.__bases__:
- if base.__name__.lower() == parent:
- return True
- return False
-
-
-class RestfulModel(dict):
- attrs = []
- date_attrs = {}
- datetime_attrs = {}
- datetime_filter_attrs = {}
- typed_dict_attrs = {}
- read_only_attrs = {}
- # The Nylas API holds most objects for an account directly under '/',
- # but some of them are under '/a' (mostly the account-management
- # and billing code). api_root is a tiny metaprogramming hack to let
- # us use the same code for both.
- api_root = None
-
- def __init__(self, cls, api):
- self.id = None
- self.cls = cls
- self.api = api
- super(RestfulModel, self).__init__()
-
- __setattr__ = dict.__setitem__
- __delattr__ = dict.__delitem__
- __getattr__ = dict.get
-
- @classmethod
- def create(cls, api, **kwargs):
- object_type = kwargs.get("object")
- cls_object_type = getattr(cls, "object_type", cls.__name__.lower())
- if (
- object_type
- and object_type != cls_object_type
- and object_type != "account"
- and not _is_subclass(cls, object_type)
- ):
- # We were given a specific object type and we're trying to
- # instantiate something different; abort. (Relevant for folders
- # and labels API.)
- # We need a special case for accounts because the /accounts API
- # is different between the open source and hosted API.
- return
- obj = cls(api) # pylint: disable=no-value-for-parameter
- obj.cls = cls
- for attr in cls.attrs:
- # Support attributes we want to override with properties where
- # the property names overlap with the JSON names (e.g. folders)
- attr_name = attr
- if attr_name.startswith("_"):
- attr = attr_name[1:]
- if attr in kwargs:
- obj[attr_name] = kwargs[attr]
- if attr_name == "from":
- obj["from_"] = kwargs[attr]
- for date_attr, iso_attr in cls.date_attrs.items():
- if kwargs.get(iso_attr):
- obj[date_attr] = datetime.strptime(kwargs[iso_attr], "%Y-%m-%d").date()
- for dt_attr, ts_attr in cls.datetime_attrs.items():
- if kwargs.get(ts_attr):
- try:
- obj[dt_attr] = datetime.utcfromtimestamp(kwargs[ts_attr])
- except TypeError:
- # If the datetime format is in the format of ISO8601
- obj[dt_attr] = datetime.strptime(
- kwargs[ts_attr], "%Y-%m-%dT%H:%M:%S.%fZ"
- )
- for attr, value_attr_name in cls.typed_dict_attrs.items():
- obj[attr] = typed_dict_attr(kwargs.get(attr, []), attr_name=value_attr_name)
-
- if "id" not in kwargs:
- obj["id"] = None
-
- return obj
-
- def as_json(self):
- dct = {}
- # 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 attr in self.cls.attrs:
- if attr in self.read_only_attrs:
- continue
- if hasattr(self, attr):
- if attr in reserved_keywords:
- dct[attr] = getattr(self, "{}_".format(attr))
- else:
- dct[attr] = getattr(self, attr)
- for date_attr, iso_attr in self.cls.date_attrs.items():
- if date_attr in self.read_only_attrs:
- continue
- if self.get(date_attr):
- dct[iso_attr] = self[date_attr].strftime("%Y-%m-%d")
- for dt_attr, ts_attr in self.cls.datetime_attrs.items():
- if dt_attr in self.read_only_attrs:
- continue
- if self.get(dt_attr):
- dct[ts_attr] = timestamp_from_dt(self[dt_attr])
- for attr, value_attr in self.cls.typed_dict_attrs.items():
- if attr in self.read_only_attrs:
- continue
- typed_dict = getattr(self, attr)
- if value_attr:
- dct[attr] = []
- for key, values in typed_dict.items():
- for value in values:
- dct[attr].append({"type": key, value_attr: value})
- else:
- dct[attr] = []
- for values in typed_dict.values():
- for value in values:
- dct[attr].append(value)
- return dct
-
-
-class NylasAPIObject(RestfulModel):
- def __init__(self, cls, api):
- RestfulModel.__init__(self, cls, api)
-
- def child_collection(self, cls, **filters):
- return RestfulModelCollection(cls, self.api, **filters)
-
- def save(self, **kwargs):
- if self.id:
- new_obj = self.api._update_resource(
- self.cls, self.id, self.as_json(), **kwargs
- )
- else:
- new_obj = self.api._create_resource(self.cls, self.as_json(), **kwargs)
- for attr in self.cls.attrs:
- if hasattr(new_obj, attr):
- setattr(self, attr, getattr(new_obj, attr))
-
- def update(self):
- new_obj = self.api._update_resource(self.cls, self.id, self.as_json())
- for attr in self.cls.attrs:
- if hasattr(new_obj, attr):
- setattr(self, attr, getattr(new_obj, attr))
-
-
-class Message(NylasAPIObject):
- attrs = [
- "bcc",
- "body",
- "cc",
- "date",
- "events",
- "files",
- "from",
- "id",
- "account_id",
- "object",
- "snippet",
- "starred",
- "subject",
- "thread_id",
- "to",
- "unread",
- "starred",
- "_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
-
-
-class Folder(NylasAPIObject):
- attrs = ["id", "display_name", "name", "object", "account_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)
-
-
-class Label(NylasAPIObject):
- attrs = ["id", "display_name", "name", "object", "account_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)
-
-
-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
-
-
-# This is a dummy class that allows us to use the create_resource function
-# and pass in a 'Send' object that will translate into a 'send' endpoint.
-class Send(Message):
- collection_name = "send"
-
- def __init__(self, api): # pylint: disable=super-init-not-called
- NylasAPIObject.__init__(
- self, Send, api
- ) # pylint: disable=non-parent-init-called
-
-
-class Draft(Message):
- attrs = [
- "bcc",
- "cc",
- "body",
- "date",
- "files",
- "from",
- "id",
- "account_id",
- "object",
- "subject",
- "thread_id",
- "to",
- "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)
-
-
-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)
-
-
-class Contact(NylasAPIObject):
- attrs = [
- "id",
- "object",
- "account_id",
- "given_name",
- "middle_name",
- "surname",
- "suffix",
- "nickname",
- "company_name",
- "job_title",
- "manager_name",
- "office_location",
- "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
-
-
-class Calendar(NylasAPIObject):
- attrs = ["id", "account_id", "name", "description", "read_only", "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)
-
-
-class Event(NylasAPIObject):
- attrs = [
- "id",
- "account_id",
- "title",
- "description",
- "conferencing",
- "location",
- "read_only",
- "when",
- "busy",
- "participants",
- "calendar_id",
- "recurrence",
- "status",
- "master_event_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)
-
- 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 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)
-
-
-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)
-
-
-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, RoomResource, api)
-
- def as_json(self):
- dct = NylasAPIObject.as_json(self)
- # "type" cannot be modified after created
- if self.id:
- dct.pop("type")
- return dct
-
-
-class Namespace(NylasAPIObject):
- attrs = [
- "account",
- "email_address",
- "id",
- "account_id",
- "object",
- "provider",
- "name",
- "organization_unit",
- ]
- collection_name = "n"
-
- def __init__(self, api):
- NylasAPIObject.__init__(self, Namespace, api)
-
- def child_collection(self, cls, **filters):
- return RestfulModelCollection(cls, self.api, self.id, **filters)
-
-
-class Account(NylasAPIObject):
- api_root = "a"
-
- attrs = [
- "account_id",
- "billing_state",
- "email",
- "id",
- "namespace_id",
- "provider",
- "sync_state",
- "trial",
- ]
-
- collection_name = "accounts"
-
- def __init__(self, api):
- NylasAPIObject.__init__(self, Account, api)
-
- def as_json(self):
- dct = NylasAPIObject.as_json(self)
- 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)
-
- def delete(self):
- raise NotImplementedError
-
-
-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
-
-
-class SingletonAccount(APIAccount):
- # This is an APIAccount that lives under /account.
- collection_name = "account"
diff --git a/nylas/config.py b/nylas/config.py
new file mode 100644
index 00000000..ee625e02
--- /dev/null
+++ b/nylas/config.py
@@ -0,0 +1,47 @@
+from enum import Enum
+from typing import TypedDict
+
+from typing_extensions import NotRequired
+
+
+class Region(str, Enum):
+ """
+ Enum representing the regions supported by the Nylas API
+ """
+
+ US = "us"
+ EU = "eu"
+
+
+class RequestOverrides(TypedDict):
+ """
+ Overrides to use for an outgoing request to the Nylas API
+
+ Attributes:
+ api_key: The API key to use for the request.
+ api_uri: The API URI to use for the request.
+ timeout: The timeout to use for the request.
+ headers: Additional headers to include in the request.
+ """
+
+ api_key: NotRequired[str]
+ api_uri: NotRequired[str]
+ timeout: NotRequired[int]
+ headers: NotRequired[dict]
+
+
+DEFAULT_REGION = Region.US
+""" The default Nylas API region. """
+
+REGION_CONFIG = {
+ Region.US: {
+ "nylasApiUrl": "https://api.us.nylas.com",
+ },
+ Region.EU: {
+ "nylasApiUrl": "https://api.eu.nylas.com",
+ },
+}
+""" The available preset configuration values for each Nylas API region. """
+
+DEFAULT_SERVER_URL = REGION_CONFIG[DEFAULT_REGION]["nylasApiUrl"]
+""" The default Nylas API URL. """
diff --git a/tests/__init__.py b/nylas/handler/__init__.py
similarity index 100%
rename from tests/__init__.py
rename to nylas/handler/__init__.py
diff --git a/nylas/handler/api_resources.py b/nylas/handler/api_resources.py
new file mode 100644
index 00000000..6badcf43
--- /dev/null
+++ b/nylas/handler/api_resources.py
@@ -0,0 +1,129 @@
+from __future__ import annotations
+
+from nylas.models.response import Response, ListResponse, DeleteResponse
+from nylas.resources.resource import Resource
+
+# pylint: disable=too-few-public-methods,missing-class-docstring,missing-function-docstring
+
+
+class ListableApiResource(Resource):
+ def list(
+ self,
+ path,
+ response_type,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ overrides=None,
+ ) -> ListResponse:
+ response_json, response_headers = self._http_client._execute(
+ "GET", path, headers, query_params, request_body, overrides=overrides
+ )
+
+ return ListResponse.from_dict(response_json, response_type, response_headers)
+
+
+class FindableApiResource(Resource):
+ def find(
+ self,
+ path,
+ response_type,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ overrides=None,
+ ) -> Response:
+ response_json, response_headers = self._http_client._execute(
+ "GET", path, headers, query_params, request_body, overrides=overrides
+ )
+
+ return Response.from_dict(response_json, response_type, response_headers)
+
+
+class CreatableApiResource(Resource):
+ def create(
+ self,
+ path,
+ response_type,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ overrides=None,
+ serialized_json_body=None,
+ ) -> Response:
+
+ kwargs = {"overrides": overrides}
+ if serialized_json_body is not None:
+ kwargs["serialized_json_body"] = serialized_json_body
+ response_json, response_headers = self._http_client._execute(
+ "POST", path, headers, query_params, request_body, **kwargs
+ )
+
+ return Response.from_dict(response_json, response_type, response_headers)
+
+
+class UpdatableApiResource(Resource):
+ def update(
+ self,
+ path,
+ response_type,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ method="PUT",
+ overrides=None,
+ serialized_json_body=None,
+ ):
+ kwargs = {"overrides": overrides}
+ if serialized_json_body is not None:
+ kwargs["serialized_json_body"] = serialized_json_body
+ response_json, response_headers = self._http_client._execute(
+ method, path, headers, query_params, request_body, **kwargs
+ )
+
+ return Response.from_dict(response_json, response_type, response_headers)
+
+
+class UpdatablePatchApiResource(Resource):
+ def patch(
+ self,
+ path,
+ response_type,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ method="PATCH",
+ overrides=None,
+ serialized_json_body=None,
+ ):
+ kwargs = {"overrides": overrides}
+ if serialized_json_body is not None:
+ kwargs["serialized_json_body"] = serialized_json_body
+ response_json, response_headers = self._http_client._execute(
+ method, path, headers, query_params, request_body, **kwargs
+ )
+
+ return Response.from_dict(response_json, response_type, response_headers)
+
+
+class DestroyableApiResource(Resource):
+ def destroy(
+ self,
+ path,
+ response_type=None,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ overrides=None,
+ ):
+ if response_type is None:
+ response_type = DeleteResponse
+
+ response_json, response_headers = self._http_client._execute(
+ "DELETE", path, headers, query_params, request_body, overrides=overrides
+ )
+
+ # Check if the response type is a dataclass_json class
+ if hasattr(response_type, "from_dict") and not hasattr(response_type, "headers"):
+ return response_type.from_dict(response_json)
+ return response_type.from_dict(response_json, headers=response_headers)
diff --git a/nylas/handler/http_client.py b/nylas/handler/http_client.py
new file mode 100644
index 00000000..023e2d10
--- /dev/null
+++ b/nylas/handler/http_client.py
@@ -0,0 +1,214 @@
+import json
+import sys
+from typing import Union, Tuple, Dict
+from urllib.parse import urlparse, quote
+
+import requests
+from requests import Response
+from requests.structures import CaseInsensitiveDict
+
+from nylas._client_sdk_version import __VERSION__
+from nylas.models.errors import (
+ NylasApiError,
+ NylasApiErrorResponse,
+ NylasSdkTimeoutError,
+ NylasOAuthError,
+ NylasOAuthErrorResponse,
+ NylasApiErrorResponseData,
+)
+
+
+def _validate_response(response: Response) -> Tuple[Dict, CaseInsensitiveDict]:
+ response_data = response.json()
+ if response.status_code >= 400:
+ parsed_url = urlparse(response.url)
+ try:
+ if (
+ "connect/token" in parsed_url.path
+ or "connect/revoke" in parsed_url.path
+ ):
+ parsed_error = NylasOAuthErrorResponse.from_dict(response_data)
+ raise NylasOAuthError(parsed_error, response.status_code, response.headers)
+
+ parsed_error = NylasApiErrorResponse.from_dict(response_data)
+ raise NylasApiError(parsed_error, response.status_code, response.headers)
+ except (KeyError, TypeError) as exc:
+ request_id = response_data.get("request_id", None)
+ raise NylasApiError(
+ NylasApiErrorResponse(
+ request_id,
+ NylasApiErrorResponseData(
+ type="unknown",
+ message=response_data,
+ ),
+ ),
+ status_code=response.status_code,
+ headers=response.headers,
+ ) from exc
+ return (response_data, response.headers)
+
+def _build_query_params(base_url: str, query_params: dict = None) -> str:
+ query_param_parts = []
+ for key, value in query_params.items():
+ if isinstance(value, list):
+ for item in value:
+ query_param_parts.append(f"{key}={quote(str(item))}")
+ elif isinstance(value, dict):
+ for k, v in value.items():
+ query_param_parts.append(f"{key}={k}:{quote(str(v))}")
+ else:
+ query_param_parts.append(f"{key}={quote(str(value))}")
+
+ query_string = "&".join(query_param_parts)
+ return f"{base_url}?{query_string}"
+
+
+# pylint: disable=too-few-public-methods
+class HttpClient:
+ """HTTP client for the Nylas API."""
+
+ def __init__(self, api_server, api_key, timeout):
+ self.api_server = api_server
+ self.api_key = api_key
+ self.timeout = timeout
+
+ def _execute(
+ self,
+ method,
+ path,
+ headers=None,
+ query_params=None,
+ request_body=None,
+ data=None,
+ overrides=None,
+ serialized_json_body=None,
+ ) -> dict:
+ request = self._build_request(
+ method,
+ path,
+ headers,
+ query_params,
+ request_body,
+ data,
+ overrides,
+ serialized_json_body=serialized_json_body,
+ )
+
+ timeout = self.timeout
+ if overrides and overrides.get("timeout"):
+ timeout = overrides["timeout"]
+
+ # Serialize request_body to JSON with ensure_ascii=False to preserve UTF-8 characters
+ # and allow_nan=True to support NaN/Infinity values (matching default json.dumps behavior).
+ # Encode as UTF-8 bytes to avoid Latin-1 encoding errors with special characters.
+ # When serialized_json_body is set (e.g. Nylas service account signing), send those exact
+ # bytes so the wire body matches the payload that was signed.
+ json_data = None
+ if serialized_json_body is not None and data is None:
+ json_data = serialized_json_body
+ elif request_body is not None and data is None:
+ json_data = json.dumps(request_body, ensure_ascii=False, allow_nan=True).encode("utf-8")
+ try:
+ response = requests.request(
+ request["method"],
+ request["url"],
+ headers=request["headers"],
+ data=json_data if json_data is not None else data,
+ timeout=timeout,
+ )
+ except requests.exceptions.Timeout as exc:
+ raise NylasSdkTimeoutError(url=request["url"], timeout=timeout) from exc
+
+ return _validate_response(response)
+
+ def _execute_download_request(
+ self,
+ path,
+ headers=None,
+ query_params=None,
+ stream=False,
+ overrides=None,
+ ) -> Union[bytes, Response,dict]:
+ request = self._build_request("GET", path, headers, query_params, overrides)
+
+ timeout = self.timeout
+ if overrides and overrides.get("timeout"):
+ timeout = overrides["timeout"]
+ try:
+ response = requests.request(
+ request["method"],
+ request["url"],
+ headers=request["headers"],
+ timeout=timeout,
+ stream=stream,
+ )
+
+ if not response.ok:
+ return _validate_response(response)
+
+ # If we stream an iterator for streaming the content, otherwise return the entire byte array
+ if stream:
+ return response
+
+ return response.content if response.content else None
+ except requests.exceptions.Timeout as exc:
+ raise NylasSdkTimeoutError(url=request["url"], timeout=timeout) from exc
+
+ def _build_request(
+ self,
+ method: str,
+ path: str,
+ headers: dict = None,
+ query_params: dict = None,
+ request_body=None,
+ data=None,
+ overrides=None,
+ serialized_json_body=None,
+ ) -> dict:
+ api_server = self.api_server
+ if overrides and overrides.get("api_uri"):
+ api_server = overrides["api_uri"]
+
+ base_url = f"{api_server}{path}"
+ url = _build_query_params(base_url, query_params) if query_params else base_url
+ body_for_content_type = (
+ request_body if request_body is not None else serialized_json_body
+ )
+ headers = self._build_headers(headers, body_for_content_type, data, overrides)
+
+ return {
+ "method": method,
+ "url": url,
+ "headers": headers,
+ }
+
+ def _build_headers(
+ self, extra_headers: dict = None, response_body=None, data=None, overrides=None
+ ) -> dict:
+ override_headers = {}
+ if overrides and overrides.get("headers"):
+ override_headers = overrides["headers"]
+
+ if extra_headers is None:
+ extra_headers = {}
+
+ major, minor, revision, _, __ = sys.version_info
+ user_agent_header = (
+ f"Nylas Python SDK {__VERSION__} - {major}.{minor}.{revision}"
+ )
+
+ api_key = self.api_key
+ if overrides and overrides.get("api_key"):
+ api_key = overrides["api_key"]
+
+ headers = {
+ "X-Nylas-API-Wrapper": "python",
+ "User-Agent": user_agent_header,
+ "Authorization": f"Bearer {api_key}",
+ }
+ if data is not None and data.content_type is not None:
+ headers["Content-type"] = data.content_type
+ elif response_body is not None:
+ headers["Content-type"] = "application/json; charset=utf-8"
+
+ return {**headers, **extra_headers, **override_headers}
diff --git a/nylas/handler/service_account.py b/nylas/handler/service_account.py
new file mode 100644
index 00000000..ce86b8d6
--- /dev/null
+++ b/nylas/handler/service_account.py
@@ -0,0 +1,136 @@
+"""
+Nylas Service Account request signing for organization admin APIs.
+
+See https://developer.nylas.com/docs/v3/auth/nylas-service-account/
+
+If you set X-Nylas-* headers manually via RequestOverrides, the HTTP request body must be
+byte-identical to the canonical JSON string used when computing the signature.
+"""
+
+from __future__ import annotations
+
+import base64
+import json
+import secrets
+import string
+import time
+from typing import Any, Dict, Optional, Tuple
+
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import padding, rsa
+
+_NONCE_ALPHABET = string.ascii_letters + string.digits
+_NONCE_LENGTH = 20
+
+
+def canonical_json(data: Dict[str, Any]) -> str:
+ """
+ Deterministic JSON with sorted keys at each object level, matching Nylas's reference
+ implementation for service account signing.
+ """
+ keys = sorted(data.keys())
+ parts = []
+ for k in keys:
+ key_json = json.dumps(k, ensure_ascii=False, allow_nan=False)
+ v = data[k]
+ if isinstance(v, dict):
+ val_json = canonical_json(v)
+ else:
+ val_json = json.dumps(
+ v, ensure_ascii=False, allow_nan=False, separators=(",", ":")
+ )
+ parts.append(f"{key_json}:{val_json}")
+ return "{" + ",".join(parts) + "}"
+
+
+def load_rsa_private_key_from_pem(pem: str) -> rsa.RSAPrivateKey:
+ """Load an RSA private key from a PEM string (PKCS#1 or PKCS#8)."""
+ key_bytes = pem.encode("utf-8") if isinstance(pem, str) else pem
+ loaded = serialization.load_pem_private_key(key_bytes, password=None)
+ if not isinstance(loaded, rsa.RSAPrivateKey):
+ raise ValueError("Private key must be RSA")
+ return loaded
+
+
+def _signing_envelope_bytes(
+ path: str,
+ method: str,
+ timestamp: int,
+ nonce: str,
+ body: Optional[Dict[str, Any]],
+) -> bytes:
+ method_l = method.lower()
+ envelope: Dict[str, Any] = {
+ "method": method_l,
+ "nonce": nonce,
+ "path": path,
+ "timestamp": timestamp,
+ }
+ if method_l in ("post", "put", "patch") and body is not None:
+ envelope["payload"] = canonical_json(body)
+ canonical = canonical_json(envelope)
+ return canonical.encode("utf-8")
+
+
+def sign_bytes(private_key: rsa.RSAPrivateKey, message: bytes) -> str:
+ """RSA PKCS#1 v1.5 signature over SHA-256(message), Base64-encoded."""
+ signature = private_key.sign(message, padding.PKCS1v15(), hashes.SHA256())
+ return base64.b64encode(signature).decode("ascii")
+
+
+def generate_nonce(length: int = _NONCE_LENGTH) -> str:
+ """Cryptographically secure nonce (alphanumeric), default length 20."""
+ return "".join(secrets.choice(_NONCE_ALPHABET) for _ in range(length))
+
+
+class ServiceAccountSigner:
+ """
+ Builds the four required Nylas service account headers for a single request.
+
+ Args:
+ private_key_pem: RSA private key in PEM text form (from the service account JSON).
+ private_key_id: Value for X-Nylas-Kid (``private_key_id`` in the JSON credentials).
+ """
+
+ def __init__(self, private_key_pem: str, private_key_id: str):
+ self._private_key = load_rsa_private_key_from_pem(private_key_pem)
+ self._private_key_id = private_key_id
+
+ def build_headers(
+ self,
+ method: str,
+ path: str,
+ body: Optional[Dict[str, Any]] = None,
+ *,
+ timestamp: Optional[int] = None,
+ nonce: Optional[str] = None,
+ ) -> Tuple[Dict[str, str], Optional[bytes]]:
+ """
+ Produce signing headers and optional canonical JSON body bytes.
+
+ For POST/PUT/PATCH, ``body`` must be the same dict that will be sent; returned bytes
+ should be passed to HttpClient as ``serialized_json_body`` so the wire body matches
+ the signed payload.
+
+ Returns:
+ (headers, serialized_json_body) where serialized_json_body is set for
+ POST/PUT/PATCH when body is not None, else None.
+ """
+ ts = int(time.time()) if timestamp is None else int(timestamp)
+ n = generate_nonce() if nonce is None else nonce
+
+ serialized: Optional[bytes] = None
+ body_for_sign: Optional[Dict[str, Any]] = body
+ if method.lower() in ("post", "put", "patch") and body is not None:
+ serialized = canonical_json(body).encode("utf-8")
+
+ envelope = _signing_envelope_bytes(path, method, ts, n, body_for_sign)
+ signature_b64 = sign_bytes(self._private_key, envelope)
+
+ headers = {
+ "X-Nylas-Kid": self._private_key_id,
+ "X-Nylas-Nonce": n,
+ "X-Nylas-Timestamp": str(ts),
+ "X-Nylas-Signature": signature_b64,
+ }
+ return headers, serialized
diff --git a/nylas/models/__init__.py b/nylas/models/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nylas/models/application_details.py b/nylas/models/application_details.py
new file mode 100644
index 00000000..c017f9d0
--- /dev/null
+++ b/nylas/models/application_details.py
@@ -0,0 +1,83 @@
+from dataclasses import dataclass, field
+from typing import Literal, Optional, List
+
+from dataclasses_json import dataclass_json
+
+from nylas.models.redirect_uri import RedirectUri
+
+Region = Literal["us", "eu"]
+""" Literal representing the available Nylas API regions. """
+
+Environment = Literal["production", "staging", "development", "sandbox"]
+""" Literal representing the different Nylas API environments. """
+
+
+@dataclass_json
+@dataclass
+class Branding:
+ """
+ Class representation of branding details for the application.
+
+ Attributes:
+ name: Name of the application.
+ icon_url: URL pointing to the application icon.
+ website_url: Application/publisher website URL.
+ description: Description of the application.
+ """
+
+ name: str
+ icon_url: Optional[str] = None
+ website_url: Optional[str] = None
+ description: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class HostedAuthentication:
+ """
+ Class representation of hosted authentication branding details.
+
+ Attributes:
+ background_image_url: URL pointing to the background image.
+ alignment: Alignment of the background image.
+ color_primary: Primary color of the hosted authentication page.
+ color_secondary: Secondary color of the hosted authentication page.
+ title: Title of the hosted authentication page.
+ subtitle: Subtitle for the hosted authentication page.
+ background_color: Background color of the hosted authentication page.
+ spacing: CSS spacing attribute in px.
+ """
+
+ background_image_url: Optional[str] = None
+ alignment: Optional[str] = None
+ color_primary: Optional[str] = None
+ color_secondary: Optional[str] = None
+ title: Optional[str] = None
+ subtitle: Optional[str] = None
+ background_color: Optional[str] = None
+ spacing: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class ApplicationDetails:
+ """
+ Class representation of a Nylas application details object.
+
+ Attributes:
+ application_id: Public application ID.
+ organization_id: ID representing the organization.
+ region: Region identifier.
+ environment: Environment identifier.
+ branding: Branding details for the application.
+ hosted_authentication: Hosted authentication branding details.
+ callback_uris: List of redirect URIs.
+ """
+
+ application_id: str
+ organization_id: str
+ region: Region
+ environment: Environment
+ branding: Branding
+ hosted_authentication: Optional[HostedAuthentication] = None
+ callback_uris: List[RedirectUri] = field(default_factory=list)
diff --git a/nylas/models/attachments.py b/nylas/models/attachments.py
new file mode 100644
index 00000000..7bd8aeb9
--- /dev/null
+++ b/nylas/models/attachments.py
@@ -0,0 +1,136 @@
+from dataclasses import dataclass
+from typing import Optional, Union, BinaryIO, Dict
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired, Literal
+
+
+@dataclass_json
+@dataclass
+class Attachment:
+ """
+ An attachment on a message.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: The grant ID of the attachment.
+ size: Size of the attachment in bytes.
+ filename: Name of the attachment.
+ content_type: MIME type of the attachment.
+ content_id: The content ID of the attachment.
+ content_disposition: The content disposition of the attachment.
+ is_inline: Whether the attachment is inline.
+ """
+
+ id: Optional[str] = None
+ grant_id: Optional[str] = None
+ filename: Optional[str] = None
+ content_type: Optional[str] = None
+ size: Optional[int] = None
+ content_id: Optional[str] = None
+ content_disposition: Optional[str] = None
+ is_inline: Optional[bool] = None
+
+
+class CreateAttachmentRequest(TypedDict):
+ """
+ A request to create an attachment.
+
+ You can use `attach_file_request_builder()` to build this request.
+
+ Attributes:
+ filename: Name of the attachment.
+ content_type: MIME type of the attachment.
+ content: Either a Base64 encoded content of the attachment or a pointer to a file.
+ size: Size of the attachment in bytes.
+ content_id: The content ID of the attachment.
+ content_disposition: The content disposition of the attachment.
+ is_inline: Whether the attachment is inline.
+ """
+
+ filename: str
+ content_type: str
+ content: Union[str, BinaryIO]
+ size: int
+ content_id: NotRequired[str]
+ content_disposition: NotRequired[str]
+ is_inline: NotRequired[bool]
+
+
+class FindAttachmentQueryParams(TypedDict):
+ """
+ Interface of the query parameters for finding an attachment.
+
+ Attributes:
+ message_id: Message ID to find the attachment in.
+ """
+
+ message_id: str
+
+
+AttachmentUploadSessionStatusType = Literal["uploading", "ready", "failed", "expired"]
+
+
+class CreateAttachmentUploadSessionRequest(TypedDict):
+ """
+ Request body for creating a large-attachment upload session (Graph only).
+
+ Attributes:
+ filename: The name of the file as it will appear in the email.
+ content_type: MIME type of the file (e.g. 'application/pdf').
+ size: Expected file size in bytes (max 157286400 / 150 MB). Recommended โ
+ Nylas validates the upload matches this size at completion.
+ """
+
+ filename: str
+ content_type: str
+ size: NotRequired[int]
+
+
+@dataclass_json
+@dataclass
+class AttachmentUploadSession:
+ """
+ Upload session returned when creating a large-attachment upload session.
+
+ Attributes:
+ attachment_id: Unique session ID โ use when completing the session and when
+ referencing the attachment in send/draft.
+ method: HTTP method to use when uploading to `url`. Always 'PUT'.
+ url: Pre-signed URL to upload file bytes (no Nylas auth header needed).
+ headers: Headers to include when uploading to `url`.
+ expires_at: When the session expires (RFC 3339).
+ max_size: Maximum allowed file size in bytes (157286400).
+ size: Expected file size echoing the request; 0 if `size` was omitted.
+ content_type: MIME type of the file.
+ filename: Name of the file.
+ grant_id: Grant ID the session belongs to.
+ """
+
+ attachment_id: Optional[str] = None
+ method: Optional[str] = None
+ url: Optional[str] = None
+ headers: Optional[Dict[str, str]] = None
+ expires_at: Optional[str] = None
+ max_size: Optional[int] = None
+ size: Optional[int] = None
+ content_type: Optional[str] = None
+ filename: Optional[str] = None
+ grant_id: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class AttachmentUploadSessionComplete:
+ """
+ Result of completing a large-attachment upload session.
+
+ Attributes:
+ attachment_id: The session ID.
+ grant_id: Grant ID the session belongs to.
+ status: Upload status; typically 'ready' after successful completion.
+ """
+
+ attachment_id: Optional[str] = None
+ grant_id: Optional[str] = None
+ status: Optional[str] = None
diff --git a/nylas/models/auth.py b/nylas/models/auth.py
new file mode 100644
index 00000000..97481e88
--- /dev/null
+++ b/nylas/models/auth.py
@@ -0,0 +1,204 @@
+from dataclasses import dataclass
+from typing import Optional, List, Literal
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+AccessType = Literal["online", "offline"]
+""" Literal for the access type of the authentication URL. """
+
+Provider = Literal["google", "imap", "microsoft", "icloud", "virtual-calendar", "yahoo", "ews", "zoom", "nylas"]
+""" Literal for the different authentication providers. """
+
+Prompt = Literal[
+ "select_provider", "detect", "select_provider,detect", "detect,select_provider"
+]
+""" Literal for the different supported OAuth prompts. """
+
+
+class URLForAuthenticationConfig(TypedDict):
+ """
+ Configuration for generating a URL for OAuth 2.0 authentication.
+
+ Attributes:
+ client_id: The client ID of your application.
+ redirect_uri: Redirect URI of the integration.
+ provider: The integration provider type that you already had set up with Nylas for this application.
+ If not set, the user is directed to the Hosted Login screen and prompted to select a provider.
+ access_type: If the exchange token should return a refresh token too.
+ Not suitable for client side or JavaScript apps.
+ prompt: The prompt parameter is used to force the consent screen to be displayed even if the user
+ has already given consent to your application.
+ scope: A space-delimited list of scopes that identify the resources that your application
+ could access on the user's behalf.
+ If no scope is given, all of the default integration's scopes are used.
+ include_grant_scopes: If set to true, the scopes granted to the application will be included in the response.
+ state: Optional state to be returned after authentication
+ login_hint: Prefill the login name (usually email) during authorization flow.
+ If a Grant for the provided email already exists, a Grant's re-auth will automatically be initiated.
+ smtp_required: If True, adds options=smtp_required so users must enter SMTP settings during
+ authentication. Relevant for IMAP; avoids grant errors when sending email later.
+ """
+
+ client_id: str
+ redirect_uri: str
+ provider: NotRequired[Provider]
+ access_type: NotRequired[AccessType]
+ prompt: NotRequired[Prompt]
+ scope: NotRequired[List[str]]
+ include_grant_scopes: NotRequired[bool]
+ state: NotRequired[str]
+ login_hint: NotRequired[str]
+ credential_id: NotRequired[str]
+ smtp_required: NotRequired[bool]
+
+
+class URLForAdminConsentConfig(URLForAuthenticationConfig):
+ """
+ Configuration for generating a URL for admin consent authentication for Microsoft.
+
+ Attributes:
+ credential_id: The credential ID for the Microsoft account
+ """
+
+ credential_id: str
+
+
+class CodeExchangeRequest(TypedDict):
+ """
+ Interface of a Nylas code exchange request
+
+ Attributes:
+ redirect_uri: Should match the same redirect URI that was used for getting the code during the initial
+ authorization request.
+ code: OAuth 2.0 code fetched from the previous step.
+ client_id: Client ID of the application.
+ client_secret: Client secret of the application. If not provided, the API Key will be used instead.
+ code_verifier: The original plain text code verifier (code_challenge) used in the initial
+ authorization request (PKCE).
+ """
+
+ redirect_uri: str
+ code: str
+ client_id: str
+ client_secret: NotRequired[str]
+ code_verifier: NotRequired[str]
+
+
+class TokenExchangeRequest(TypedDict):
+ """
+ Interface of a Nylas token exchange request
+
+ Attributes:
+ redirect_uri: Should match the same redirect URI that was used for getting the code during the initial
+ authorization request.
+ refresh_token: Token to refresh/request your short-lived access token
+ client_id: Client ID of the application.
+ client_secret: Client secret of the application. If not provided, the API Key will be used instead.
+ """
+
+ redirect_uri: str
+ refresh_token: str
+ client_id: str
+ client_secret: NotRequired[str]
+
+
+@dataclass_json
+@dataclass
+class CodeExchangeResponse:
+ """
+ Class representation of a Nylas code exchange response.
+
+ Attributes:
+ access_token: Supports exchanging the Nylas code for an access token, or refreshing an access token.
+ grant_id: ID representing the new Grant.
+ scope: List of scopes associated with the token.
+ expires_in: The remaining lifetime of the access token, in seconds.
+ email: Email address of the grant that is created.
+ refresh_token: Returned only if the code is requested using "access_type=offline".
+ id_token: A JWT that contains identity information about the user. Digitally signed by Nylas.
+ token_type: Always "Bearer".
+ provider: The provider that the code was exchanged with.
+ """
+
+ access_token: str
+ grant_id: str
+ expires_in: int
+ email: Optional[str] = None
+ refresh_token: Optional[str] = None
+ scope: Optional[str] = None
+ id_token: Optional[str] = None
+ token_type: Optional[str] = None
+ provider: Optional[Provider] = None
+
+
+@dataclass_json
+@dataclass
+class TokenInfoResponse:
+ """
+ Class representation of a Nylas token information response.
+
+ Attributes:
+ iss: The issuer of the token.
+ aud: The token's audience.
+ iat: The time that the token was issued.
+ exp: The time that the token expires.
+ sub: The token's subject.
+ email: The email address of the Grant belonging to the user's token.
+ """
+
+ iss: str
+ aud: str
+ iat: int
+ exp: int
+ sub: Optional[str] = None
+ email: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class PkceAuthUrl:
+ """
+ Class representing the object containing the OAuth 2.0 URL and the hashed secret.
+
+ Attributes:
+ secret: Server-side challenge used in the OAuth 2.0 flow.
+ secret_hash: SHA-256 hash of the secret.
+ url: The URL for hosted authentication.
+ """
+
+ secret: str
+ secret_hash: str
+ url: str
+
+
+class ProviderDetectParams(TypedDict):
+ """
+ Interface representing the object used to set parameters for detecting a provider.
+
+ Attributes:
+ email: Email address to detect the provider for.
+ all_provider_types: Search by all providers regardless of created integrations. If unset, defaults to false.
+ """
+
+ email: str
+ all_provider_types: NotRequired[bool]
+
+
+@dataclass_json
+@dataclass
+class ProviderDetectResponse:
+ """
+ Interface representing the Nylas provider detect response.
+
+ Attributes:
+ email_address: Email provided for autodetection
+ detected: Whether the provider was detected
+ provider: Detected provider
+ type: Provider type (if IMAP provider detected displays the IMAP provider)
+ """
+
+ email_address: str
+ detected: bool
+ provider: Optional[str] = None
+ type: Optional[str] = None
diff --git a/nylas/models/availability.py b/nylas/models/availability.py
new file mode 100644
index 00000000..87255ffc
--- /dev/null
+++ b/nylas/models/availability.py
@@ -0,0 +1,169 @@
+from dataclasses import dataclass, field
+from typing import List, Literal
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+AvailabilityMethod = Literal["max-fairness", "max-availability"]
+""" Literal representing the method used to determine availability for a meeting. """
+
+
+@dataclass_json
+@dataclass
+class TimeSlot:
+ """
+ Interface for a Nylas availability time slot
+
+ Attributes:
+ emails: The emails of the participants who are available for the time slot.
+ start_time: Unix timestamp for the start of the slot.
+ end_time: Unix timestamp for the end of the slot.
+ """
+
+ emails: List[str]
+ start_time: int
+ end_time: int
+
+
+@dataclass_json
+@dataclass
+class GetAvailabilityResponse:
+ """
+ Interface for a Nylas get availability response
+
+ Attributes:
+ order: This property is only populated for round-robin events.
+ It will contain the order in which the accounts would be next in line to attend the proposed meeting.
+ time_slots: The available time slots where a new meeting can be created for the requested preferences.
+ """
+
+ time_slots: List[TimeSlot]
+ order: List[str] = field(default_factory=list)
+
+
+class MeetingBuffer(TypedDict):
+ """
+ Interface for the meeting buffer object within an availability request.
+
+ Attributes:
+ before: The amount of buffer time in increments of 5 minutes to add before existing meetings.
+ Defaults to 0.
+ after: The amount of buffer time in increments of 5 minutes to add after existing meetings.
+ Defaults to 0.
+ """
+
+ before: int
+ after: int
+
+
+class OpenHours(TypedDict):
+ """
+ Interface of a participant's open hours.
+
+ Attributes:
+ days: The days of the week that the open hour settings will be applied to.
+ Sunday corresponds to 0 and Saturday corresponds to 6.
+ timezone: IANA time zone database formatted string (e.g. America/New_York).
+ start: Start time in 24-hour time format. Leading 0's are left off.
+ end: End time in 24-hour time format. Leading 0's are left off.
+ extdates: A list of dates that will be excluded from the open hours.
+ Dates should be formatted as YYYY-MM-DD.
+ """
+
+ days: List[int]
+ timezone: str
+ start: str
+ end: str
+ exdates: NotRequired[List[str]]
+
+
+class SpecificTimeAvailability(TypedDict):
+ """
+ Interface of a participant's availability for specific dates.
+ Overrides open_hours for the specified dates.
+
+ Attributes:
+ dates: The date in ISO 8601 format.
+ start: Start time in 24-hour time format. Leading 0's are left off.
+ end: End time in 24-hour time format. Leading 0's are left off.
+ """
+
+ date: str
+ start: str
+ end: str
+
+
+class AvailabilityRules(TypedDict):
+ """
+ Interface for the availability rules for a Nylas calendar.
+
+ Attributes:
+ availability_method: The method used to determine availability for a meeting.
+ buffer: The buffer to add to the start and end of a meeting.
+ default_open_hours: A default set of open hours to apply to all participants.
+ You can overwrite these open hours for individual participants by specifying open_hours on
+ the participant object.
+ round_robin_group_id: The ID on events that Nylas considers when calculating the order of
+ round-robin participants.
+ This is used for both max-fairness and max-availability methods.
+ tentative_as_busy: Controls whether tentative calendar events should be treated as busy time.
+ When set to false, tentative events will be considered as free in availability calculations.
+ Defaults to true. Only applicable for Microsoft and EWS calendar providers.
+ """
+
+ availability_method: NotRequired[AvailabilityMethod]
+ buffer: NotRequired[MeetingBuffer]
+ default_open_hours: NotRequired[List[OpenHours]]
+ round_robin_group_id: NotRequired[str]
+ tentative_as_busy: NotRequired[bool]
+
+
+class AvailabilityParticipant(TypedDict):
+ """
+ Interface of participant details to check availability for.
+
+ Attributes:
+ email: The email address of the participant.
+ calendar_ids: An optional list of the calendar IDs associated with each participant's email address.
+ If not provided, Nylas uses the primary calendar ID.
+ open_hours: Open hours for this participant. The endpoint searches for free time slots during these open hours.
+ specific_time_availability: Specific availability for this participant that overrides open_hours
+ for the specified dates.
+ """
+
+ email: str
+ calendar_ids: NotRequired[List[str]]
+ open_hours: NotRequired[List[OpenHours]]
+ specific_time_availability: NotRequired[List[SpecificTimeAvailability]]
+
+
+class GetAvailabilityRequest(TypedDict):
+ """
+ Interface for a Nylas get availability request
+
+ Attributes:
+ start_time: Unix timestamp for the start time to check availability for.
+ end_time: Unix timestamp for the end time to check availability for.
+ participants: Participant details to check availability for.
+ duration_minutes: The total number of minutes the event should last.
+ interval_minutes: Nylas checks from the nearest interval of the passed start time.
+ For example, to schedule 30-minute meetings with 15 minutes between them.
+ If you have a meeting starting at 9:59, the API returns times starting at 10:00. 10:00-10:30, 10:15-10:45.
+ round_to_30_minutes: When set to true, the availability time slots will start at 30 minutes past or on the hour.
+ For example, a free slot starting at 16:10 is considered available only from 16:30.
+ Note: This field is deprecated, use round_to instead.
+ availability_rules: The rules to apply when checking availability.
+ round_to: The number of minutes to round the time slots to.
+ This allows for rounding to any multiple of 5 minutes, up to a maximum of 60 minutes.
+ The default value is set to 15 minutes.
+ When this variable is assigned a value, it overrides the behavior of the roundTo30Minutes flag,if it was set
+ """
+
+ start_time: int
+ end_time: int
+ participants: List[AvailabilityParticipant]
+ duration_minutes: int
+ interval_minutes: NotRequired[int]
+ round_to_30_minutes: NotRequired[bool]
+ availability_rules: NotRequired[AvailabilityRules]
+ round_to: NotRequired[int]
diff --git a/nylas/models/calendars.py b/nylas/models/calendars.py
new file mode 100644
index 00000000..95dbf6f9
--- /dev/null
+++ b/nylas/models/calendars.py
@@ -0,0 +1,247 @@
+from dataclasses import dataclass
+from typing import Dict, Any, Optional, List
+from enum import Enum
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+class EventSelection(str, Enum):
+ """
+ Enum representing the different types of events to include for notetaking.
+
+ Values:
+ INTERNAL: Events where the host domain matches all participants' domain names
+ EXTERNAL: Events where the host domain differs from any participant's domain name
+ OWN_EVENTS: Events where the host is the same as the user's grant
+ PARTICIPANT_ONLY: Events where the user's grant is a participant but not the host
+ ALL: When all options are included, all events with meeting links will have Notetakers
+ """
+ INTERNAL = "internal"
+ EXTERNAL = "external"
+ OWN_EVENTS = "own_events"
+ PARTICIPANT_ONLY = "participant_only"
+ ALL = "all"
+
+
+@dataclass_json
+@dataclass
+class NotetakerParticipantFilter:
+ """
+ Class representation of Notetaker participant filter settings.
+
+ Attributes:
+ participants_gte: Only have meeting bot join meetings with greater than or equal to this number of participants.
+ participants_lte: Only have meeting bot join meetings with less than or equal to this number of participants.
+ """
+ participants_gte: Optional[int] = None
+ participants_lte: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class NotetakerRules:
+ """
+ Class representation of Notetaker rules for joining meetings.
+
+ Attributes:
+ event_selection: Types of events to include for notetaking.
+ participant_filter: Filters to apply based on the number of participants.
+ """
+ event_selection: Optional[List[EventSelection]] = None
+ participant_filter: Optional[NotetakerParticipantFilter] = None
+
+
+@dataclass_json
+@dataclass
+class NotetakerMeetingSettings:
+ """
+ Class representation of Notetaker meeting settings.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ """
+ video_recording: Optional[bool] = True
+ audio_recording: Optional[bool] = True
+ transcription: Optional[bool] = True
+
+
+@dataclass_json
+@dataclass
+class CalendarNotetaker:
+ """
+ Class representation of Notetaker settings for a calendar.
+
+ Attributes:
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ rules: Rules for when the Notetaker should join a meeting.
+ """
+ name: Optional[str] = "Nylas Notetaker"
+ meeting_settings: Optional[NotetakerMeetingSettings] = None
+ rules: Optional[NotetakerRules] = None
+
+
+@dataclass_json
+@dataclass
+class Calendar:
+ """
+ Class representation of a Nylas Calendar object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: Grant ID representing the user's account.
+ name: Name of the Calendar.
+ timezone: IANA time zone database-formatted string (for example, "America/New_York").
+ This value is only supported for Google and Virtual Calendars.
+ read_only: If the event participants are able to edit the Event.
+ is_owned_by_user: If the Calendar is owned by the user account.
+ object: The type of object.
+ description: Description of the Calendar.
+ location: Geographic location of the Calendar as free-form text.
+ hex_color: The background color of the calendar in the hexadecimal format (for example, "#0099EE").
+ If not defined, the default color is used.
+ hex_foreground_color: The background color of the calendar in the hexadecimal format (for example, "#0099EE").
+ If not defined, the default color is used (Google only).
+ is_primary: If the Calendar is the account's primary calendar.
+ metadata: A list of key-value pairs storing additional data.
+ notetaker: Notetaker meeting bot settings for the calendar.
+ """
+
+ id: str
+ grant_id: str
+ name: str
+ read_only: bool
+ is_owned_by_user: bool
+ object: str = "calendar"
+ timezone: Optional[str] = None
+ description: Optional[str] = None
+ location: Optional[str] = None
+ hex_color: Optional[str] = None
+ hex_foreground_color: Optional[str] = None
+ is_primary: Optional[bool] = None
+ metadata: Optional[Dict[str, Any]] = None
+ notetaker: Optional[CalendarNotetaker] = None
+
+
+class ListCalendarsQueryParams(ListQueryParams):
+ """
+ Interface of the query parameters for listing calendars.
+
+ Attributes:
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ metadata_pair: Pass in your metadata key-value pair to search for metadata.
+ select (NotRequired[str]): Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ metadata_pair: NotRequired[Dict[str, str]]
+
+
+class FindCalendarQueryParams(TypedDict):
+ """
+ Interface of the query parameters for finding a calendar.
+
+ Attributes:
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ select: NotRequired[str]
+
+
+class NotetakerCalendarSettings(TypedDict):
+ """
+ Interface for Notetaker meeting settings for a calendar.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ """
+ video_recording: NotRequired[bool]
+ audio_recording: NotRequired[bool]
+ transcription: NotRequired[bool]
+
+
+class NotetakerCalendarParticipantFilter(TypedDict):
+ """
+ Interface for Notetaker participant filter settings.
+
+ Attributes:
+ participants_gte: Only have meeting bot join meetings with greater than or equal to this number of participants.
+ participants_lte: Only have meeting bot join meetings with less than or equal to this number of participants.
+ """
+ participants_gte: NotRequired[int]
+ participants_lte: NotRequired[int]
+
+
+class NotetakerCalendarRules(TypedDict):
+ """
+ Interface for Notetaker rules for joining meetings.
+
+ Attributes:
+ event_selection: Types of events to include for notetaking.
+ participant_filter: Filters to apply based on the number of participants.
+ """
+ event_selection: NotRequired[List[EventSelection]]
+ participant_filter: NotRequired[NotetakerCalendarParticipantFilter]
+
+
+class NotetakerCalendarRequest(TypedDict):
+ """
+ Interface for Notetaker settings in a calendar request.
+
+ Attributes:
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ rules: Rules for when the Notetaker should join a meeting.
+ """
+ name: NotRequired[str]
+ meeting_settings: NotRequired[NotetakerCalendarSettings]
+ rules: NotRequired[NotetakerCalendarRules]
+
+
+class CreateCalendarRequest(TypedDict):
+ """
+ Interface of a Nylas create calendar request
+
+ Attributes:
+ name: Name of the Calendar.
+ description: Description of the calendar.
+ location: Geographic location of the calendar as free-form text.
+ timezone: IANA time zone database formatted string (e.g. America/New_York).
+ metadata: A list of key-value pairs storing additional data.
+ notetaker: Notetaker meeting bot settings.
+ """
+
+ name: str
+ description: NotRequired[str]
+ location: NotRequired[str]
+ timezone: NotRequired[str]
+ metadata: NotRequired[Dict[str, str]]
+ notetaker: NotRequired[NotetakerCalendarRequest]
+
+
+class UpdateCalendarRequest(CreateCalendarRequest):
+ """
+ Interface of a Nylas update calendar request
+
+ Attributes:
+ hexColor: The background color of the calendar in the hexadecimal format (e.g. #0099EE).
+ Empty indicates default color.
+ hexForegroundColor: The background color of the calendar in the hexadecimal format (e.g. #0099EE).
+ Empty indicates default color. (Google only)
+ notetaker: Notetaker meeting bot settings.
+ """
+
+ hexColor: NotRequired[str]
+ hexForegroundColor: NotRequired[str]
+ notetaker: NotRequired[NotetakerCalendarRequest]
diff --git a/nylas/models/connectors.py b/nylas/models/connectors.py
new file mode 100644
index 00000000..65eb3306
--- /dev/null
+++ b/nylas/models/connectors.py
@@ -0,0 +1,159 @@
+from dataclasses import dataclass
+from typing import Dict, Any, List, Optional, Union
+from typing_extensions import TypedDict, NotRequired
+
+from dataclasses_json import dataclass_json
+
+from nylas.models.auth import Provider
+from nylas.models.list_query_params import ListQueryParams
+
+
+@dataclass_json
+@dataclass
+class Connector:
+ """
+ Interface representing the Nylas connector response.
+
+ Attributes:
+ provider: The provider type
+ settings: Optional settings from provider
+ scope: Default scopes for the connector
+ """
+
+ provider: Provider
+ settings: Optional[Dict[str, Any]] = None
+ scope: Optional[List[str]] = None
+
+
+class BaseCreateConnectorRequest(TypedDict):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ provider: The provider type
+ """
+
+ provider: Provider
+ active_credential_id: NotRequired[str]
+
+
+class GoogleCreateConnectorSettings(TypedDict):
+ """
+ Interface representing a Google connector creation request.
+
+ Attributes:
+ client_id: The Google Client ID
+ client_secret: The Google Client Secret
+ topic_name: The Google Pub/Sub topic name
+ """
+
+ client_id: str
+ client_secret: str
+ topic_name: NotRequired[str]
+
+
+class MicrosoftCreateConnectorSettings(TypedDict):
+ """
+ Interface representing a Microsoft connector creation request.
+
+ Attributes:
+ client_id: The Google Client ID
+ client_secret: The Google Client Secret
+ tenant: The Microsoft tenant ID
+ """
+
+ client_id: str
+ client_secret: str
+ tenant: NotRequired[str]
+
+
+class GoogleCreateConnectorRequest(BaseCreateConnectorRequest):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ provider (Provider): The provider type, should be Google
+ settings: The Google OAuth provider credentials and settings
+ scope: The Google OAuth scopes
+ """
+
+ settings: GoogleCreateConnectorSettings
+ scope: NotRequired[List[str]]
+
+
+class MicrosoftCreateConnectorRequest(BaseCreateConnectorRequest):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ name (str): Custom name of the connector
+ provider (Provider): The provider type, should be Google
+ settings: The Microsoft OAuth provider credentials and settings
+ scope: The Microsoft OAuth scopes
+ """
+
+ settings: MicrosoftCreateConnectorSettings
+ scope: NotRequired[List[str]]
+
+
+class ImapCreateConnectorRequest(BaseCreateConnectorRequest):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ name (str): Custom name of the connector
+ provider (Provider): The provider type, should be IMAP
+ """
+
+ pass
+
+
+class VirtualCalendarsCreateConnectorRequest(BaseCreateConnectorRequest):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ name (str): Custom name of the connector
+ provider (Provider): The provider type
+ """
+
+ pass
+
+
+CreateConnectorRequest = Union[
+ GoogleCreateConnectorRequest,
+ MicrosoftCreateConnectorRequest,
+ ImapCreateConnectorRequest,
+ VirtualCalendarsCreateConnectorRequest,
+]
+""" The type of the Nylas connector creation request. """
+
+
+class UpdateConnectorRequest(TypedDict):
+ """
+ Interface representing the base Nylas connector creation request.
+
+ Attributes:
+ name: Custom name of the connector
+ settings: The OAuth provider credentials and settings
+ scope: The OAuth scopes
+ """
+
+ name: NotRequired[str]
+ settings: NotRequired[Dict[str, Any]]
+ scope: NotRequired[List[str]]
+ active_credential_id: NotRequired[str]
+
+
+class ListConnectorQueryParams(ListQueryParams):
+ """
+ Interface of the query parameters for listing connectors.
+
+ Attributes:
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ """
+
+ pass
diff --git a/nylas/models/contacts.py b/nylas/models/contacts.py
new file mode 100644
index 00000000..9ddcc1cf
--- /dev/null
+++ b/nylas/models/contacts.py
@@ -0,0 +1,388 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import Optional, List
+from typing_extensions import TypedDict, NotRequired
+
+from dataclasses_json import dataclass_json
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+class SourceType(str, Enum):
+ """Enum representing the different types of sources for a contact."""
+
+ ADDRESS_BOOK = "address_book"
+ INBOX = "inbox"
+ DOMAIN = "domain"
+
+
+@dataclass_json
+@dataclass
+class PhoneNumber:
+ """
+ A phone number for a contact.
+
+ Attributes:
+ number: The phone number.
+ type: The type of phone number.
+ """
+
+ number: Optional[str] = None
+ type: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class PhysicalAddress:
+ """
+ A physical address for a contact.
+
+ Attributes:
+ format: The format of the address.
+ street_address: The street address of the contact.
+ city: The city of the contact.
+ postal_code: The postal code of the contact.
+ state: The state of the contact.
+ country: The country of the contact.
+ type: The type of address.
+ """
+
+ format: Optional[str] = None
+ street_address: Optional[str] = None
+ city: Optional[str] = None
+ postal_code: Optional[str] = None
+ state: Optional[str] = None
+ country: Optional[str] = None
+ type: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class WebPage:
+ """
+ A web page for a contact.
+
+ Attributes:
+ url: The URL of the web page.
+ type: The type of web page.
+ """
+
+ url: Optional[str] = None
+ type: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class ContactEmail:
+ """
+ An email address for a contact.
+
+ Attributes:
+ email: The email address.
+ type: The type of email address.
+ """
+
+ email: Optional[str] = None
+ type: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class ContactGroupId:
+ """
+ A contact group ID for a contact.
+
+ Attributes:
+ id: The contact group ID.
+ """
+
+ id: str
+
+
+@dataclass_json
+@dataclass
+class InstantMessagingAddress:
+ """
+ An instant messaging address for a contact.
+
+ Attributes:
+ im_address: The instant messaging address.
+ type: The type of instant messaging address.
+ """
+
+ im_address: Optional[str] = None
+ type: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class Contact:
+ """
+ Class representation of a Nylas contact object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: Grant ID representing the user's account.
+ object: The type of object.
+ birthday: The contact's birthday.
+ company_name: The contact's company name.
+ display_name: The contact's display name.
+ emails: The contact's email addresses.
+ im_addresses: The contact's instant messaging addresses.
+ given_name: The contact's given name.
+ job_title: The contact's job title.
+ manager_name: The contact's manager name.
+ middle_name: The contact's middle name.
+ nickname: The contact's nickname.
+ notes: The contact's notes.
+ office_location: The contact's office location.
+ picture_url: The contact's picture URL.
+ picture: The contact's picture.
+ suffix: The contact's suffix.
+ surname: The contact's surname.
+ source: The contact's source.
+ phone_numbers: The contact's phone numbers.
+ physical_addresses: The contact's physical addresses.
+ web_pages: The contact's web pages.
+ groups: The contact's groups.
+ """
+
+ id: str
+ grant_id: str
+ object: str = "contact"
+ birthday: Optional[str] = None
+ company_name: Optional[str] = None
+ display_name: Optional[str] = None
+ emails: Optional[List[ContactEmail]] = None
+ im_addresses: Optional[List[InstantMessagingAddress]] = None
+ given_name: Optional[str] = None
+ job_title: Optional[str] = None
+ manager_name: Optional[str] = None
+ middle_name: Optional[str] = None
+ nickname: Optional[str] = None
+ notes: Optional[str] = None
+ office_location: Optional[str] = None
+ picture_url: Optional[str] = None
+ picture: Optional[str] = None
+ suffix: Optional[str] = None
+ surname: Optional[str] = None
+ source: Optional[SourceType] = None
+ phone_numbers: Optional[List[PhoneNumber]] = None
+ physical_addresses: Optional[List[PhysicalAddress]] = None
+ web_pages: Optional[List[WebPage]] = None
+ groups: Optional[List[ContactGroupId]] = None
+
+
+class FindContactQueryParams(TypedDict):
+ """
+ The available query parameters for finding a contact.
+ Attributes:
+ profile_picture: If true and picture_url is present, the response includes a Base64 binary data blob that
+ you can use to view information as an image file.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ profile_picture: NotRequired[bool]
+ select: NotRequired[str]
+
+
+class WriteablePhoneNumber(TypedDict):
+ """
+ A phone number for a contact.
+
+ Attributes:
+ number: The phone number.
+ type: The type of phone number.
+ """
+
+ number: NotRequired[str]
+ type: NotRequired[str]
+
+
+class WriteablePhysicalAddress(TypedDict):
+ """
+ A physical address for a contact.
+
+ Attributes:
+ format: The format of the address.
+ street_address: The street address of the contact.
+ city: The city of the contact.
+ postal_code: The postal code of the contact.
+ state: The state of the contact.
+ country: The country of the contact.
+ type: The type of address.
+ """
+
+ format: NotRequired[str]
+ street_address: NotRequired[str]
+ city: NotRequired[str]
+ postal_code: NotRequired[str]
+ state: NotRequired[str]
+ country: NotRequired[str]
+ type: NotRequired[str]
+
+
+class WriteableWebPage(TypedDict):
+ """
+ A web page for a contact.
+
+ Attributes:
+ url: The URL of the web page.
+ type: The type of web page.
+ """
+
+ url: NotRequired[str]
+ type: NotRequired[str]
+
+
+class WriteableContactEmail(TypedDict):
+ """
+ An email address for a contact.
+
+ Attributes:
+ email: The email address.
+ type: The type of email address.
+ """
+
+ email: NotRequired[str]
+ type: NotRequired[str]
+
+
+class WriteableContactGroupId(TypedDict):
+ """
+ A contact group ID for a contact.
+
+ Attributes:
+ id: The contact group ID.
+ """
+
+ id: str
+
+
+class WriteableInstantMessagingAddress(TypedDict):
+ """
+ An instant messaging address for a contact.
+
+ Attributes:
+ im_address: The instant messaging address.
+ type: The type of instant messaging address.
+ """
+
+ im_address: NotRequired[str]
+ type: NotRequired[str]
+
+
+class CreateContactRequest(TypedDict):
+ """
+ Interface for creating a Nylas contact.
+
+ Attributes:
+ birthday: The contact's birthday.
+ company_name: The contact's company name.
+ display_name: The contact's display name.
+ emails: The contact's email addresses.
+ im_addresses: The contact's instant messaging addresses.
+ given_name: The contact's given name.
+ job_title: The contact's job title.
+ manager_name: The contact's manager name.
+ middle_name: The contact's middle name.
+ nickname: The contact's nickname.
+ notes: The contact's notes.
+ office_location: The contact's office location.
+ picture_url: The contact's picture URL.
+ picture: The contact's picture.
+ suffix: The contact's suffix.
+ surname: The contact's surname.
+ source: The contact's source.
+ phone_numbers: The contact's phone numbers.
+ physical_addresses: The contact's physical addresses.
+ web_pages: The contact's web pages.
+ groups: The contact's groups.
+ """
+
+ birthday: NotRequired[str]
+ company_name: NotRequired[str]
+ display_name: NotRequired[str]
+ emails: NotRequired[List[WriteableContactEmail]]
+ im_addresses: NotRequired[List[WriteableInstantMessagingAddress]]
+ given_name: NotRequired[str]
+ job_title: NotRequired[str]
+ manager_name: NotRequired[str]
+ middle_name: NotRequired[str]
+ nickname: NotRequired[str]
+ notes: NotRequired[str]
+ office_location: NotRequired[str]
+ picture_url: NotRequired[str]
+ picture: NotRequired[str]
+ suffix: NotRequired[str]
+ surname: NotRequired[str]
+ source: NotRequired[SourceType]
+ phone_numbers: NotRequired[List[WriteablePhoneNumber]]
+ physical_addresses: NotRequired[List[WriteablePhysicalAddress]]
+ web_pages: NotRequired[List[WriteableWebPage]]
+ groups: NotRequired[List[WriteableContactGroupId]]
+
+
+UpdateContactRequest = CreateContactRequest
+"""Interface for updating a Nylas contact."""
+
+
+class ListContactsQueryParams(ListQueryParams):
+ """
+ Interface representing the query parameters for listing contacts.
+
+ Attributes:
+ email: Return contacts with matching email address.
+ phone_number: Return contacts with matching phone number.
+ source: Return contacts from a specific source.
+ group: Return contacts from a specific group.
+ recurse: Return contacts from all sub-groups of the specified group.
+ select (NotRequired[str]): Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ """
+
+ email: NotRequired[str]
+ phone_number: NotRequired[str]
+ source: NotRequired[SourceType]
+ group: NotRequired[str]
+ recurse: NotRequired[bool]
+
+
+class GroupType(str, Enum):
+ """Enum representing the different types of contact groups."""
+
+ USER = "user"
+ SYSTEM = "system"
+ OTHER = "other"
+
+
+@dataclass_json
+@dataclass
+class ContactGroup:
+ """
+ Class representation of a Nylas contact group object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: Grant ID representing the user's account.
+ object: The type of object.
+ group_type: The type of contact group.
+ name: The name of the contact group.
+ path: The path of the contact group.
+ """
+
+ id: str
+ grant_id: str
+ object: str = "contact_group"
+ group_type: Optional[GroupType] = None
+ name: Optional[str] = None
+ path: Optional[str] = None
+
+
+ListContactGroupsQueryParams = ListQueryParams
+"""The available query parameters for listing contact groups."""
diff --git a/nylas/models/credentials.py b/nylas/models/credentials.py
new file mode 100644
index 00000000..39dd65ff
--- /dev/null
+++ b/nylas/models/credentials.py
@@ -0,0 +1,111 @@
+from dataclasses import dataclass
+from typing import Dict, Optional, Literal, Union
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, Protocol, NotRequired
+
+CredentialType = Literal["adminconsent", "serviceaccount", "connector"]
+"""The alias for the different types of credentials that can be created."""
+
+
+@dataclass_json
+@dataclass
+class Credential:
+ """
+ Interface representing a Nylas Credential object.
+ Attributes
+ id: Globally unique object identifier;
+ name: Name of the credential
+ credential_type: The type of credential
+ hashed_data: Hashed value of the credential that you created
+ created_at: Timestamp of when the credential was created
+ updated_at: Timestamp of when the credential was updated;
+ """
+
+ id: str
+ name: str
+ credential_type: Optional[CredentialType] = None
+ hashed_data: Optional[str] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+
+
+class MicrosoftAdminConsentSettings(Protocol):
+ """
+ Interface representing the data required to create a Microsoft Admin Consent credential.
+
+ Attributes:
+ client_id: The client ID of the Azure AD application
+ client_secret: The client secret of the Azure AD application
+ """
+
+ client_id: str
+ client_secret: str
+
+
+class GoogleServiceAccountCredential(Protocol):
+ """
+ Interface representing the data required to create a Google Service Account credential.
+
+ Attributes:
+ private_key_id: The private key ID of the service account
+ private_key: The private key of the service account
+ client_email: The client email of the service account
+ """
+
+ private_key_id: str
+ private_key: str
+ client_email: str
+
+
+CredentialData = Union[
+ MicrosoftAdminConsentSettings, GoogleServiceAccountCredential, Dict[str, any]
+]
+"""The alias for the different types of credential data that can be used to create a credential."""
+
+
+class CredentialRequest(TypedDict):
+ """
+ Interface representing a request to create a credential.
+
+ Attributes:
+ name: Name of the credential
+ credential_type: Type of credential you want to create.
+ credential_data: The data required to successfully create the credential object
+ """
+
+ name: Optional[str]
+ credential_type: CredentialType
+ credential_data: CredentialData
+
+
+class UpdateCredentialRequest(TypedDict):
+ """
+ Interface representing a request to update a credential.
+
+ Attributes:
+ name: Name of the credential
+ credential_data: The data required to successfully create the credential object
+ """
+
+ name: Optional[str]
+ credential_data: Optional[CredentialData]
+
+
+class ListCredentialQueryParams(TypedDict):
+ """
+ Interface representing the query parameters for credentials .
+
+ Attributes:
+ offset: Offset results
+ sort_by: Sort entries by field name
+ order_by: Order results by the specified field.
+ Currently only start is supported.
+ limit: The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ """
+
+ limit: NotRequired[int]
+ offset: NotRequired[int]
+ order_by: NotRequired[str]
+ sort_by: NotRequired[str]
diff --git a/nylas/models/domains.py b/nylas/models/domains.py
new file mode 100644
index 00000000..cca1b7c1
--- /dev/null
+++ b/nylas/models/domains.py
@@ -0,0 +1,100 @@
+from dataclasses import dataclass, field
+from typing import Any, Literal, Optional
+
+from dataclasses_json import config, dataclass_json
+from typing_extensions import TypedDict
+
+from nylas.models.list_query_params import ListQueryParams
+
+DomainVerificationType = Literal["ownership", "dkim", "spf", "feedback", "mx"]
+
+
+class ListDomainsQueryParams(ListQueryParams):
+ """
+ Query parameters for listing domains.
+
+ Attributes:
+ limit: Maximum number of objects to return.
+ page_token: Cursor for the next page (from ``next_cursor`` on the previous response).
+ """
+
+ pass
+
+
+class CreateDomainRequest(TypedDict):
+ """Request body for registering a domain."""
+
+ name: str
+ domain_address: str
+
+
+class UpdateDomainRequest(TypedDict, total=False):
+ """Request body for updating a domain (currently only ``name`` is supported)."""
+
+ name: str
+
+
+class GetDomainInfoRequest(TypedDict):
+ """Request body for retrieving DNS records for a verification type."""
+
+ type: DomainVerificationType
+
+
+class VerifyDomainRequest(TypedDict):
+ """Request body for triggering DNS verification."""
+
+ type: DomainVerificationType
+
+
+@dataclass_json
+@dataclass
+class Domain:
+ """
+ A domain registered for Transactional Send or Nylas Inbound.
+ """
+
+ id: str
+ name: str
+ branded: bool
+ domain_address: str
+ organization_id: str
+ region: str
+ verified_ownership: bool
+ verified_dkim: bool
+ verified_spf: bool
+ verified_mx: bool
+ verified_feedback: bool
+ verified_dmarc: bool
+ verified_arc: bool
+ created_at: int
+ updated_at: int
+
+
+@dataclass_json
+@dataclass
+class DomainVerificationAttempt:
+ """
+ DNS verification attempt or required records for a verification type.
+ """
+
+ verification_type: Optional[str] = field(
+ default=None, metadata=config(field_name="type")
+ )
+ options: Optional[Any] = None
+ host: Optional[str] = None
+ value: Optional[str] = None
+ status: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class DomainVerificationDetails:
+ """
+ Response data from get domain info or verify domain endpoints.
+ """
+
+ domain_id: str
+ attempt: Optional[DomainVerificationAttempt] = None
+ created_at: Optional[int] = None
+ expires_at: Optional[int] = None
+ message: Optional[str] = None
diff --git a/nylas/models/drafts.py b/nylas/models/drafts.py
new file mode 100644
index 00000000..78626b95
--- /dev/null
+++ b/nylas/models/drafts.py
@@ -0,0 +1,193 @@
+from dataclasses import dataclass
+from typing import List, Dict, Any, get_type_hints
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+from nylas.models.attachments import CreateAttachmentRequest
+from nylas.models.events import EmailName
+from nylas.models.list_query_params import ListQueryParams
+from nylas.models.messages import Message
+
+
+@dataclass_json
+@dataclass
+class Draft(Message):
+ """
+ A Draft object.
+
+ Attributes:
+ id (str): Globally unique object identifier.
+ grant_id (str): The grant that this message belongs to.
+ from_ (List[EmailName]): The sender of the message.
+ date (int): The date the message was received.
+ object: The type of object.
+ thread_id (Optional[str]): The thread that this message belongs to.
+ subject (Optional[str]): The subject of the message.
+ to (Optional[List[EmailName]]): The recipients of the message.
+ cc (Optional[List[EmailName]]): The CC recipients of the message.
+ bcc (Optional[List[EmailName]]): The BCC recipients of the message.
+ reply_to (Optional[List[EmailName]]): The reply-to recipients of the message.
+ unread (Optional[bool]): Whether the message is unread.
+ starred (Optional[bool]): Whether the message is starred.
+ snippet (Optional[str]): A snippet of the message body.
+ body (Optional[str]): The body of the message.
+ attachments (Optional[List[Attachment]]): The attachments on the message.
+ folders (Optional[List[str]]): The folders that the message is in.
+ created_at (Optional[int]): Unix timestamp of when the message was created.
+ """
+
+ object: str = "draft"
+
+
+class TrackingOptions(TypedDict):
+ """
+ The different tracking options for when a message is sent.
+
+ Attributes:
+ label: The label to apply to tracked messages.
+ links: Whether to track links.
+ opens: Whether to track opens.
+ thread_replies: Whether to track thread replies.
+ """
+
+ label: NotRequired[str]
+ links: NotRequired[bool]
+ opens: NotRequired[bool]
+ thread_replies: NotRequired[bool]
+
+
+class CustomHeader(TypedDict):
+ """
+ A key-value pair representing a header that can be added to drafts and outgoing messages.
+
+ Attributes:
+ name: The name of the custom header.
+ value: The value of the custom header.
+ """
+
+ name: str
+ value: str
+
+
+class CreateDraftRequest(TypedDict):
+ """
+ A request to create a draft.
+
+ Attributes:
+ subject: The subject of the message.
+ to: The recipients of the message.
+ cc: The CC recipients of the message.
+ bcc: The BCC recipients of the message.
+ reply_to: The reply-to recipients of the message.
+ starred: Whether the message is starred.
+ body: The body of the message.
+ attachments: The attachments on the message.
+ send_at: Unix timestamp to send the message at.
+ reply_to_message_id: The ID of the message that you are replying to.
+ tracking_options: Options for tracking opens, links, and thread replies.
+ custom_headers: Custom headers to add to the message.
+ metadata: A dictionary of key-value pairs storing additional data.
+ is_plaintext: When true, the message body is sent as plain text and the MIME data doesn't include
+ the HTML version of the message. When false, the message body is sent as HTML.
+ """
+
+ body: NotRequired[str]
+ subject: NotRequired[str]
+ to: NotRequired[List[EmailName]]
+ bcc: NotRequired[List[EmailName]]
+ cc: NotRequired[List[EmailName]]
+ reply_to: NotRequired[List[EmailName]]
+ attachments: NotRequired[List[CreateAttachmentRequest]]
+ starred: NotRequired[bool]
+ send_at: NotRequired[int]
+ reply_to_message_id: NotRequired[str]
+ tracking_options: NotRequired[TrackingOptions]
+ custom_headers: NotRequired[List[CustomHeader]]
+ metadata: NotRequired[Dict[str, Any]]
+ is_plaintext: NotRequired[bool]
+
+
+UpdateDraftRequest = CreateDraftRequest
+""" A request to update a draft. """
+
+
+# Need to use Functional typed dicts because "from" and "in" are Python
+# keywords, and can't be declared using the declarative syntax
+ListDraftsQueryParams = TypedDict(
+ "ListDraftsQueryParams",
+ {
+ **get_type_hints(ListQueryParams),
+ "subject": NotRequired[str],
+ "any_email": NotRequired[List[str]],
+ "from": NotRequired[List[str]],
+ "to": NotRequired[List[str]],
+ "cc": NotRequired[List[str]],
+ "bcc": NotRequired[List[str]],
+ "in": NotRequired[List[str]],
+ "unread": NotRequired[bool],
+ "starred": NotRequired[bool],
+ "thread_id": NotRequired[str],
+ "has_attachment": NotRequired[bool],
+ "metadata_pair": NotRequired[str],
+ },
+)
+"""
+Query parameters for listing drafts.
+
+Attributes:
+ subject: Return messages with matching subject.
+ any_email: Return messages that have been sent or received by this comma-separated list of email addresses.
+ from: Return messages sent from this email address.
+ to: Return messages sent to this email address.
+ cc: Return messages cc'd to this email address.
+ bcc: Return messages bcc'd to this email address.
+ in: Return messages in this specific folder or label, specified by ID.
+ unread: Filter messages by unread status.
+ starred: Filter messages by starred status.
+ has_attachment: Filter messages by whether they have an attachment.
+ metadata_pair (NotRequired[str]): Filter messages by metadata key/value pair.
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+"""
+
+
+class FindDraftQueryParams(TypedDict):
+ """
+ Query parameters for finding a draft.
+
+ Attributes:
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ select: NotRequired[str]
+
+
+class SendMessageRequest(CreateDraftRequest):
+ """
+ A request to send a message.
+
+ Attributes:
+ subject (NotRequired[str]): The subject of the message.
+ to (NotRequired[List[EmailName]]): The recipients of the message.
+ cc (NotRequired[List[EmailName]]): The CC recipients of the message.
+ bcc (NotRequired[List[EmailName]]): The BCC recipients of the message.
+ reply_to (NotRequired[List[EmailName]]): The reply-to recipients of the message.
+ starred (NotRequired[bool]): Whether the message is starred.
+ body (NotRequired[str]): The body of the message.
+ attachments (NotRequired[List[CreateAttachmentRequest]]): The attachments on the message.
+ send_at (NotRequired[int]): Unix timestamp to send the message at.
+ reply_to_message_id (NotRequired[str]): The ID of the message that you are replying to.
+ tracking_options (NotRequired[TrackingOptions]): Options for tracking opens, links, and thread replies.
+ custom_headers(NotRequired[List[CustomHeader]]): Custom headers to add to the message.
+ is_plaintext (NotRequired[bool]): When true, the message body is sent as plain text and the MIME data
+ doesn't include the HTML version of the message. When false, the message body is sent as HTML.
+ from_: The sender of the message.
+ use_draft: Whether or not to use draft support. This is primarily used when dealing with large attachments.
+ """
+
+ from_: NotRequired[List[EmailName]]
+ use_draft: NotRequired[bool]
diff --git a/nylas/models/errors.py b/nylas/models/errors.py
new file mode 100644
index 00000000..43e02d4b
--- /dev/null
+++ b/nylas/models/errors.py
@@ -0,0 +1,171 @@
+from dataclasses import dataclass
+from typing import Optional
+
+from requests.structures import CaseInsensitiveDict
+from dataclasses_json import dataclass_json
+
+
+class AbstractNylasApiError(Exception):
+ """
+ Base class for all Nylas API errors.
+
+ Attributes:
+ request_id: The unique identifier of the request.
+ status_code: The HTTP status code of the error response.
+ headers: The headers returned from the API.
+ """
+
+ def __init__(
+ self,
+ message: str,
+ request_id: Optional[str] = None,
+ status_code: Optional[int] = None,
+ headers: Optional[CaseInsensitiveDict] = None,
+ ):
+ """
+ Args:
+ request_id: The unique identifier of the request.
+ status_code: The HTTP status code of the error response.
+ message: The error message.
+ """
+ self.request_id: str = request_id
+ self.status_code: int = status_code
+ self.headers: CaseInsensitiveDict = headers
+ super().__init__(message)
+
+
+class AbstractNylasSdkError(Exception):
+ """
+ Base class for all Nylas SDK errors.
+ """
+
+ pass
+
+
+@dataclass_json
+@dataclass
+class NylasApiErrorResponseData:
+ """
+ Interface representing the error data within the response object.
+
+ Attributes:
+ type: The type of error.
+ message: The error message.
+ provider_error: The provider error if there is one.
+ """
+
+ type: str
+ message: str
+ provider_error: Optional[dict] = None
+
+
+@dataclass_json
+@dataclass
+class NylasApiErrorResponse:
+ """
+ Interface representing the error response from the Nylas API.
+
+ Attributes:
+ request_id: The unique identifier of the request.
+ error: The error data.
+ """
+
+ request_id: str
+ error: NylasApiErrorResponseData
+
+
+@dataclass_json
+@dataclass
+class NylasOAuthErrorResponse:
+ """
+ Interface representing an OAuth error returned by the Nylas API.
+
+ Attributes:
+ error: Error type.
+ error_code: Error code used for referencing the docs, logs, and data stream.
+ error_description: Human readable error description.
+ error_uri: URL to the related documentation and troubleshooting regarding this error.
+ """
+
+ error: str
+ error_code: int
+ error_description: str
+ error_uri: str
+
+
+class NylasApiError(AbstractNylasApiError):
+ """
+ Class representation of a general Nylas API error.
+
+ Attributes:
+ type: Error type.
+ provider_error: Provider Error.
+ headers: The headers returned from the API.
+ """
+
+ def __init__(
+ self,
+ api_error: NylasApiErrorResponse,
+ status_code: Optional[int] = None,
+ headers: Optional[CaseInsensitiveDict] = None,
+ ):
+ """
+ Args:
+ api_error: The error details from the API.
+ status_code: The HTTP status code of the error response.
+ """
+ super().__init__(api_error.error.message, api_error.request_id, status_code, headers)
+ self.type: str = api_error.error.type
+ self.provider_error: Optional[dict] = api_error.error.provider_error
+ self.headers: CaseInsensitiveDict = headers
+
+class NylasOAuthError(AbstractNylasApiError):
+ """
+ Class representation of an OAuth error returned by the Nylas API.
+
+ Attributes:
+ error: Error type.
+ error_code: Error code used for referencing the docs, logs, and data stream.
+ error_description: Human readable error description.
+ error_uri: URL to the related documentation and troubleshooting regarding this error.
+ """
+
+ def __init__(
+ self,
+ oauth_error: NylasOAuthErrorResponse,
+ status_code: Optional[int] = None,
+ headers: Optional[CaseInsensitiveDict] = None,
+ ):
+ """
+ Args:
+ oauth_error: The error details from the API.
+ status_code: The HTTP status code of the error response.
+ """
+ super().__init__(oauth_error.error_description, None, status_code, headers)
+ self.error: str = oauth_error.error
+ self.error_code: int = oauth_error.error_code
+ self.error_description: str = oauth_error.error_description
+ self.error_uri: str = oauth_error.error_uri
+ self.headers: CaseInsensitiveDict = headers
+
+class NylasSdkTimeoutError(AbstractNylasSdkError):
+ """
+ Error thrown when the Nylas SDK times out before receiving a response from the server.
+
+ Attributes:
+ url: The URL that timed out.
+ timeout: The timeout value set in the Nylas SDK, in seconds.
+ """
+
+ def __init__(self, url: str, timeout: int, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Args:
+ url: The URL that timed out.
+ timeout: The timeout value set in the Nylas SDK, in seconds.
+ """
+ super().__init__(
+ "Nylas SDK timed out before receiving a response from the server."
+ )
+ self.url: str = url
+ self.timeout: int = timeout
+ self.headers: CaseInsensitiveDict = headers
diff --git a/nylas/models/events.py b/nylas/models/events.py
new file mode 100644
index 00000000..3fa7686e
--- /dev/null
+++ b/nylas/models/events.py
@@ -0,0 +1,936 @@
+from dataclasses import dataclass, field
+from typing import Dict, Any, List, Optional, Union, Literal
+
+from dataclasses_json import dataclass_json, config
+from typing_extensions import TypedDict, NotRequired
+
+from nylas.models.list_query_params import ListQueryParams
+
+Status = Literal["confirmed", "tentative", "cancelled"]
+""" Literal representing the status of an Event. """
+
+Visibility = Literal["default", "public", "private"]
+""" Literal representation of visibility of the Event. """
+
+ParticipantStatus = Literal["noreply", "yes", "no", "maybe"]
+""" Literal representing the status of an Event participant. """
+
+SendRsvpStatus = Literal["yes", "no", "maybe"]
+""" Literal representing the status of an RSVP. """
+
+EventType = Literal["default", "outOfOffice", "focusTime", "workingLocation"]
+""" Literal representing the event type to filter by. """
+
+
+@dataclass_json
+@dataclass
+class Participant:
+ """
+ Interface representing an Event participant.
+
+ Attributes:
+ email: Participant's email address. Required for all providers except Microsoft.
+ name: Participant's name.
+ status: Participant's status.
+ comment: Comment by the participant.
+ phone_number: Participant's phone number.
+ """
+
+ email: Optional[str] = None
+ status: Optional[ParticipantStatus] = None
+ name: Optional[str] = None
+ comment: Optional[str] = None
+ phone_number: Optional[str] = None
+
+
+class EmailName(TypedDict):
+ """
+ Interface representing an email address and optional name.
+
+ Attributes:
+ email: Email address.
+ name: Full name.
+ """
+
+ email: str
+ name: NotRequired[str]
+
+
+@dataclass_json
+@dataclass
+class Time:
+ """
+ Class representation of a specific point in time.
+ A meeting at 2pm would be represented as a time subobject.
+
+ Attributes:
+ time: A UNIX timestamp representing the time of occurrence.
+ timezone: If timezone is present, then the value for time will be read with timezone.
+ Timezone using IANA formatted string. (e.g. "America/New_York")
+ """
+
+ time: int
+ timezone: Optional[str] = None
+ object: str = "time"
+
+
+@dataclass_json
+@dataclass
+class Timespan:
+ """
+ Class representation of a time span with start and end times.
+ An hour lunch meeting would be represented as timespan subobjects.
+
+ Attributes:
+ start_time: The Event's start time.
+ end_time: The Event's end time.
+ start_timezone: The timezone of the start time, represented by an IANA-formatted string
+ (for example, "America/New_York").
+ end_timezone: The timezone of the end time, represented by an IANA-formatted string
+ (for example, "America/New_York").
+ """
+
+ start_time: int
+ end_time: int
+ start_timezone: Optional[str] = None
+ end_timezone: Optional[str] = None
+ object: str = "timespan"
+
+
+@dataclass_json
+@dataclass
+class Date:
+ """
+ Class representation of an entire day spans without specific times.
+ Your birthday and holidays would be represented as date subobjects.
+
+ Attributes:
+ date: Date of occurrence in ISO 8601 format.
+ """
+
+ date: str
+ object: str = "date"
+
+
+@dataclass_json
+@dataclass
+class Datespan:
+ """
+ Class representation of a specific dates without clock-based start or end times.
+ A business quarter or academic semester would be represented as datespan subobjects.
+
+ Attributes:
+ start_date: The start date in ISO 8601 format.
+ end_date: The end date in ISO 8601 format.
+ """
+
+ start_date: str
+ end_date: str
+ object: str = "datespan"
+
+
+When = Union[Time, Timespan, Date, Datespan]
+""" Union type representing the different types of Event time configurations. """
+
+
+def _decode_when(when: dict) -> When:
+ """
+ Decode a when object into a When object.
+
+ Args:
+ when: The when object to decode.
+
+ Returns:
+ The decoded When object.
+ """
+ if "object" not in when:
+ raise ValueError("Invalid when object, no 'object' field found.")
+
+ if when["object"] == "time":
+ return Time.from_dict(when)
+
+ if when["object"] == "timespan":
+ return Timespan.from_dict(when)
+
+ if when["object"] == "date":
+ return Date.from_dict(when)
+
+ if when["object"] == "datespan":
+ return Datespan.from_dict(when)
+
+ raise ValueError(
+ f"Invalid when object, unknown 'object' field found: {when['object']}"
+ )
+
+
+ConferencingProvider = Literal[
+ "Google Meet", "Zoom Meeting", "Microsoft Teams", "GoToMeeting", "WebEx", "unknown"
+]
+""" Literal for the different conferencing providers. """
+
+
+@dataclass_json
+@dataclass
+class DetailsConfig:
+ """
+ Class representation of a conferencing details config object
+
+ Attributes:
+ meeting_code: The conferencing meeting code. Used for Zoom.
+ password: The conferencing meeting password. Used for Zoom.
+ url: The conferencing meeting url.
+ pin: The conferencing meeting pin. Used for Google Meet.
+ phone: The conferencing meeting phone numbers. Used for Google Meet.
+ """
+
+ meeting_code: Optional[str] = None
+ password: Optional[str] = None
+ url: Optional[str] = None
+ pin: Optional[str] = None
+ phone: Optional[List[str]] = None
+
+
+@dataclass_json
+@dataclass
+class Details:
+ """
+ Class representation of a conferencing details object
+
+ Attributes:
+ provider: The conferencing provider
+ details: The conferencing details
+ """
+
+ provider: ConferencingProvider
+ details: Dict[str, Any]
+
+
+@dataclass_json
+@dataclass
+class Autocreate:
+ """
+ Class representation of a conferencing autocreate object
+
+ Attributes:
+ provider: The conferencing provider
+ autocreate: Empty dict to indicate an intention to autocreate a video link.
+ Additional provider settings may be included in autocreate.settings, but Nylas does not validate these.
+ """
+
+ provider: ConferencingProvider
+ autocreate: Dict[str, Any]
+
+
+Conferencing = Union[Details, Autocreate]
+""" Union type representing the different types of conferencing configurations. """
+
+
+def _decode_conferencing(conferencing: dict) -> Union[Conferencing, None]:
+ """
+ Decode a conferencing object into a Conferencing object.
+
+ Args:
+ conferencing: The conferencing object to decode.
+
+ Returns:
+ The decoded Conferencing object, or None if empty or incomplete.
+ """
+ if not conferencing:
+ return None
+
+ # Handle details case - must have provider to be valid
+ if "details" in conferencing and "provider" in conferencing:
+ return Details.from_dict(conferencing)
+
+ # Handle autocreate case - must have provider to be valid
+ if "autocreate" in conferencing and "provider" in conferencing:
+ return Autocreate.from_dict(conferencing)
+
+ # Handle case where provider exists but details/autocreate doesn't
+ if "provider" in conferencing:
+ # Create a Details object with empty details
+ details_dict = {
+ "provider": conferencing["provider"],
+ "details": (
+ conferencing.get("conf_settings", {})
+ if "conf_settings" in conferencing
+ else {}
+ ),
+ }
+ return Details.from_dict(details_dict)
+
+ # Handle unknown or incomplete conferencing objects by returning None
+ # This provides backwards compatibility for malformed conferencing data
+ return None
+
+
+@dataclass_json
+@dataclass
+class ReminderOverride:
+ """
+ Class representation of a reminder override object.
+
+ Attributes:
+ reminder_minutes: The user's preferred Event reminder time, in minutes.
+ Reminder minutes are in the following format: "[20]".
+ reminder_method: The user's preferred method for Event reminders (Google only).
+ """
+
+ reminder_minutes: Optional[int] = None
+ reminder_method: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class Reminders:
+ """
+ Class representation of a reminder object.
+
+ Attributes:
+ use_default: Whether to use the default reminder settings for the calendar.
+ overrides: A list of reminders for the event if use_default is set to false.
+ If left empty or omitted while use_default is set to false, the event will have no reminders.
+ """
+
+ use_default: bool
+ overrides: Optional[List[ReminderOverride]] = None
+
+
+@dataclass_json
+@dataclass
+class NotetakerMeetingSettings:
+ """
+ Class representing Notetaker meeting settings.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ """
+
+ video_recording: Optional[bool] = True
+ audio_recording: Optional[bool] = True
+ transcription: Optional[bool] = True
+
+
+@dataclass_json
+@dataclass
+class EventNotetaker:
+ """
+ Class representing Notetaker settings for an event.
+
+ Attributes:
+ id: The Notetaker bot ID.
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ """
+
+ id: Optional[str] = None
+ name: Optional[str] = "Nylas Notetaker"
+ meeting_settings: Optional[NotetakerMeetingSettings] = None
+
+
+@dataclass_json
+@dataclass
+class Event:
+ """
+ Class representation of a Nylas Event object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: Grant ID representing the user's account.
+ calendar_id: The Event's Calendar ID.
+ busy: Whether to show this Event's time block as available on shared or public calendars.
+ read_only: If the Event's participants are able to edit the Event.
+ created_at: Unix timestamp representing the Event's creation time.
+ updated_at: Unix timestamp representing the time when the Event was last updated.
+ participants: List of participants invited to the Event. Participants may be people, rooms, or resources.
+ when: Representation of an Event's time and duration.
+ conferencing: Representation of an Event's conferencing details.
+ object: The type of object.
+ description: The Event's description.
+ location: The Event's location (for example, a physical address or a meeting room).
+ ical_uid: Unique ID for iCalendar standard, allowing you to identify events across calendaring systems.
+ Recurring events may share the same value. Can be "null" for events synced before the year 2020.
+ title: The Event's title.
+ html_link: A link to the Event in the provider's UI.
+ hide_participants: Whether participants of the Event should be hidden.
+ metadata: List of key-value pairs storing additional data.
+ creator: The user who created the Event.
+ organizer: The organizer of the Event.
+ recurrence: A list of RRULE and EXDATE strings.
+ reminders: List of reminders for the Event.
+ status: The Event's status.
+ visibility: The Event's visibility (private or public).
+ capacity: Sets the maximum number of participants that may attend the event.
+ master_event_id: For recurring events, this field contains the main (master) event's ID.
+ notetaker: Notetaker meeting bot settings.
+ """
+
+ id: str
+ grant_id: str
+ calendar_id: str
+ busy: bool
+ participants: List[Participant]
+ when: When = field(metadata=config(decoder=_decode_when))
+ conferencing: Optional[Conferencing] = field(
+ default=None, metadata=config(decoder=_decode_conferencing)
+ )
+ object: str = "event"
+ visibility: Optional[Visibility] = None
+ read_only: Optional[bool] = None
+ description: Optional[str] = None
+ location: Optional[str] = None
+ ical_uid: Optional[str] = None
+ title: Optional[str] = None
+ html_link: Optional[str] = None
+ hide_participants: Optional[bool] = None
+ metadata: Optional[Dict[str, Any]] = None
+ creator: Optional[EmailName] = None
+ organizer: Optional[EmailName] = None
+ recurrence: Optional[List[str]] = None
+ reminders: Optional[Reminders] = None
+ status: Optional[Status] = None
+ capacity: Optional[int] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+ master_event_id: Optional[str] = None
+ notetaker: Optional[EventNotetaker] = None
+
+
+class CreateParticipant(TypedDict):
+ """
+ Interface representing a participant for event creation.
+
+ Attributes:
+ email: Participant's email address.
+ name: Participant's name.
+ comment: Comment by the participant.
+ phone_number: Participant's phone number.
+ """
+
+ email: NotRequired[str]
+ name: NotRequired[str]
+ comment: NotRequired[str]
+ phone_number: NotRequired[str]
+
+
+class UpdateParticipant(TypedDict):
+ """
+ Interface representing a participant for updating an event.
+
+ Attributes:
+ email: Participant's email address.
+ name: Participant's name.
+ comment: Comment by the participant.
+ phoneNumber: Participant's phone number.
+ """
+
+ email: NotRequired[str]
+ name: NotRequired[str]
+ comment: NotRequired[str]
+ phoneNumber: NotRequired[str]
+
+
+class WritableDetailsConfig(TypedDict):
+ """
+ Interface representing a writable conferencing details config object
+
+ Attributes:
+ meeting_code: The conferencing meeting code. Used for Zoom.
+ password: The conferencing meeting password. Used for Zoom.
+ url: The conferencing meeting url.
+ pin: The conferencing meeting pin. Used for Google Meet.
+ phone: The conferencing meeting phone numbers. Used for Google Meet.
+ """
+
+ meeting_code: NotRequired[str]
+ password: NotRequired[str]
+ url: NotRequired[str]
+ pin: NotRequired[str]
+ phone: NotRequired[List[str]]
+
+
+class WriteableReminderOverride(TypedDict):
+ """
+ Interface representing a writable reminder override object.
+
+ Attributes:
+ reminder_minutes: The user's preferred Event reminder time, in minutes.
+ Reminder minutes are in the following format: "[20]".
+ reminder_method: The user's preferred method for Event reminders (Google only).
+ """
+
+ reminder_minutes: NotRequired[int]
+ reminder_method: NotRequired[str]
+
+
+class CreateReminders(TypedDict):
+ """
+ Interface representing a reminder object for event creation.
+
+ Attributes:
+ use_default: Whether to use the default reminder settings for the calendar.
+ overrides: A list of reminders for the event if use_default is set to false.
+ If left empty or omitted while use_default is set to false, the event will have no reminders.
+ """
+
+ use_default: bool
+ overrides: NotRequired[List[WriteableReminderOverride]]
+
+
+class UpdateReminders(TypedDict):
+ """
+ Interface representing a reminder object for updating an event.
+
+ Attributes:
+ use_default: Whether to use the default reminder settings for the calendar.
+ overrides: A list of reminders for the event if use_default is set to false.
+ If left empty or omitted while use_default is set to false, the event will have no reminders.
+ """
+
+ use_default: NotRequired[bool]
+ overrides: NotRequired[List[WriteableReminderOverride]]
+
+
+class CreateDetails(TypedDict):
+ """
+ Interface representing a conferencing details object for event creation
+
+ Attributes:
+ provider: The conferencing provider
+ details: The conferencing details
+ """
+
+ provider: ConferencingProvider
+ details: WritableDetailsConfig
+
+
+class UpdateDetails(TypedDict):
+ """
+ Interface representing a conferencing details object for updating an event
+
+ Attributes:
+ provider: The conferencing provider
+ details: The conferencing details
+ """
+
+ provider: NotRequired[ConferencingProvider]
+ details: NotRequired[WritableDetailsConfig]
+
+
+class CreateAutocreate(TypedDict):
+ """
+ Interface representing a conferencing autocreate object for event creation
+
+ Attributes:
+ provider: The conferencing provider
+ autocreate: Empty dict to indicate an intention to autocreate a video link.
+ Additional provider settings may be included in autocreate.settings, but Nylas does not validate these.
+ """
+
+ provider: ConferencingProvider
+ autocreate: Dict[str, Any]
+
+
+class UpdateAutocreate(TypedDict):
+ """
+ Interface representing a conferencing autocreate object for event creation
+
+ Attributes:
+ provider: The conferencing provider
+ autocreate: Empty dict to indicate an intention to autocreate a video link.
+ Additional provider settings may be included in autocreate.settings, but Nylas does not validate these.
+ """
+
+ provider: NotRequired[ConferencingProvider]
+ autocreate: NotRequired[Dict[str, Any]]
+
+
+CreateConferencing = Union[CreateDetails, CreateAutocreate]
+""" Union type representing the different types of conferencing configurations for Event creation. """
+
+UpdateConferencing = Union[UpdateDetails, UpdateAutocreate]
+""" Union type representing the different types of conferencing configurations for updating an Event."""
+
+
+# When
+class CreateTime(TypedDict):
+ """
+ Interface representing a specific point in time for event creation.
+ A meeting at 2pm would be represented as a time subobject.
+
+ Attributes:
+ time: A UNIX timestamp representing the time of occurrence.
+ timezone: If timezone is present, then the value for time will be read with timezone.
+ Timezone using IANA formatted string. (e.g. "America/New_York")
+ """
+
+ time: int
+ timezone: NotRequired[str]
+
+
+class UpdateTime(TypedDict):
+ """
+ Interface representing a specific point in time for updating an event.
+ A meeting at 2pm would be represented as a time subobject.
+
+ Attributes:
+ time: A UNIX timestamp representing the time of occurrence.
+ timezone: If timezone is present, then the value for time will be read with timezone.
+ Timezone using IANA formatted string. (e.g. "America/New_York")
+ """
+
+ time: NotRequired[int]
+ timezone: NotRequired[str]
+
+
+class CreateTimespan(TypedDict):
+ """
+ Interface representing a time span with start and end times for event creation.
+ An hour lunch meeting would be represented as timespan subobjects.
+
+ Attributes:
+ start_time: The start time of the event.
+ end_time: The end time of the event.
+ start_timezone: The timezone of the start time. Timezone using IANA formatted string. (e.g. "America/New_York")
+ end_timezone: The timezone of the end time. Timezone using IANA formatted string. (e.g. "America/New_York")
+ """
+
+ start_time: int
+ end_time: int
+ start_timezone: NotRequired[str]
+ end_timezone: NotRequired[str]
+
+
+class UpdateTimespan(TypedDict):
+ """
+ Interface representing a time span with start and end times for updating an event.
+ An hour lunch meeting would be represented as timespan subobjects.
+
+ Attributes:
+ start_time: The start time of the event.
+ end_time: The end time of the event.
+ start_timezone: The timezone of the start time. Timezone using IANA formatted string. (e.g. "America/New_York")
+ end_timezone: The timezone of the end time. Timezone using IANA formatted string. (e.g. "America/New_York")
+ """
+
+ start_time: NotRequired[int]
+ end_time: NotRequired[int]
+ start_timezone: NotRequired[str]
+ end_timezone: NotRequired[str]
+
+
+class CreateDate(TypedDict):
+ """
+ Interface representing an entire day spans without specific times for event creation.
+ Your birthday and holidays would be represented as date subobjects.
+
+ Attributes:
+ date: Date of occurrence in ISO 8601 format.
+ """
+
+ date: str
+
+
+class UpdateDate(TypedDict):
+ """
+ Interface representing an entire day spans without specific times for updating an event.
+ Your birthday and holidays would be represented as date subobjects.
+
+ Attributes:
+ date: Date of occurrence in ISO 8601 format.
+ """
+
+ date: NotRequired[str]
+
+
+class CreateDatespan(TypedDict):
+ """
+ Interface representing a specific dates without clock-based start or end times for event creation.
+ A business quarter or academic semester would be represented as datespan subobjects.
+
+ Attributes:
+ start_date: The start date in ISO 8601 format.
+ end_date: The end date in ISO 8601 format.
+ """
+
+ start_date: str
+ end_date: str
+
+
+class UpdateDatespan(TypedDict):
+ """
+ Interface representing a specific dates without clock-based start or end times for updating an event.
+ A business quarter or academic semester would be represented as datespan subobjects.
+
+ Attributes:
+ start_date: The start date in ISO 8601 format.
+ end_date: The end date in ISO 8601 format.
+ """
+
+ start_date: NotRequired[str]
+ end_date: NotRequired[str]
+
+
+CreateWhen = Union[CreateTime, CreateTimespan, CreateDate, CreateDatespan]
+""" Union type representing the different types of event time configurations for Event creation. """
+
+UpdateWhen = Union[UpdateTime, UpdateTimespan, UpdateDate, UpdateDatespan]
+""" Union type representing the different types of event time configurations for updating an Event."""
+
+
+class EventNotetakerSettings(TypedDict):
+ """
+ Interface representing Notetaker meeting settings for an event.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ """
+
+ video_recording: NotRequired[bool]
+ audio_recording: NotRequired[bool]
+ transcription: NotRequired[bool]
+
+
+class EventNotetakerRequest(TypedDict):
+ """
+ Interface representing Notetaker settings for an event.
+
+ Attributes:
+ id: The Notetaker bot ID.
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ """
+
+ id: NotRequired[str]
+ name: NotRequired[str]
+ meeting_settings: NotRequired[EventNotetakerSettings]
+
+
+class CreateEventNotetaker(TypedDict):
+ """
+ Class representing Notetaker settings for an event.
+
+ Attributes:
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ """
+
+ name: Optional[str] = "Nylas Notetaker"
+ meeting_settings: Optional[EventNotetakerSettings] = None
+
+class CreateEventRequest(TypedDict):
+ """
+ Interface representing a request to create an event.
+
+ Attributes:
+ when: When the event occurs.
+ title: The title of the event.
+ busy: Whether the event is busy or free.
+ description: The description of the event.
+ location: The location of the event.
+ conferencing: The conferencing details of the event.
+ reminders: A list of reminders to send for the event.
+ If left empty or omitted, the event uses the provider defaults.
+ metadata: Metadata associated with the event.
+ participants: The participants of the event.
+ recurrence: The recurrence rules of the event.
+ visibility: The visibility of the event.
+ capacity: The capacity of the event.
+ hide_participants: Whether to hide participants of the event.
+ notetaker: Notetaker meeting bot settings.
+ """
+
+ when: CreateWhen
+ title: NotRequired[str]
+ busy: NotRequired[bool]
+ description: NotRequired[str]
+ location: NotRequired[str]
+ conferencing: NotRequired[CreateConferencing]
+ reminders: NotRequired[CreateReminders]
+ metadata: NotRequired[Dict[str, Any]]
+ participants: NotRequired[List[CreateParticipant]]
+ recurrence: NotRequired[List[str]]
+ visibility: NotRequired[Visibility]
+ capacity: NotRequired[int]
+ hide_participants: NotRequired[bool]
+ notetaker: NotRequired[CreateEventNotetaker]
+
+
+class UpdateEventRequest(TypedDict):
+ """
+ Interface representing a request to update an event.
+
+ Attributes:
+ when: When the event occurs.
+ title: The title of the event.
+ busy: Whether the event is busy or free.
+ description: The description of the event.
+ location: The location of the event.
+ conferencing: The conferencing details of the event.
+ reminders: A list of reminders to send for the event.
+ metadata: Metadata associated with the event.
+ participants: The participants of the event.
+ recurrence: The recurrence rules of the event.
+ visibility: The visibility of the event.
+ capacity: The capacity of the event.
+ hide_participants: Whether to hide participants of the event.
+ notetaker: Notetaker meeting bot settings.
+ """
+
+ when: NotRequired[UpdateWhen]
+ title: NotRequired[str]
+ busy: NotRequired[bool]
+ description: NotRequired[str]
+ location: NotRequired[str]
+ conferencing: NotRequired[UpdateConferencing]
+ reminders: NotRequired[UpdateReminders]
+ metadata: NotRequired[Dict[str, Any]]
+ participants: NotRequired[List[UpdateParticipant]]
+ recurrence: NotRequired[List[str]]
+ visibility: NotRequired[Visibility]
+ capacity: NotRequired[int]
+ hide_participants: NotRequired[bool]
+ notetaker: NotRequired[EventNotetakerRequest]
+
+
+class ListEventQueryParams(ListQueryParams):
+ """
+ Interface representing the query parameters for listing events.
+
+ Attributes:
+ calendar_id: Specify calendar ID of the event. "primary" is a supported value
+ indicating the user's primary calendar.
+ show_cancelled: Return events that have a status of cancelled.
+ If an event is recurring, then it returns no matter the value set.
+ Different providers have different semantics for cancelled events.
+ title: Return events matching the specified title.
+ description: Return events matching the specified description.
+ location: Return events matching the specified location.
+ start: Return events starting after the specified unix timestamp.
+ Defaults to the current timestamp. Not respected by metadata filtering.
+ end: Return events ending before the specified unix timestamp.
+ Defaults to a month from now. Not respected by metadata filtering.
+ metadata_pair: Pass in your metadata key and value pair to search for metadata.
+ expand_recurring: If true, the response will include an event for each occurrence of a recurring event within
+ the requested time range.
+ If false, only a single primary event will be returned for each recurring event.
+ Cannot be used when filtering on metadata.
+ busy: Returns events with a busy status of true.
+ order_by: Order results by the specified field.
+ Currently only start is supported.
+ event_type: (Google only) Filter events by event type.
+ You can pass the query parameter multiple times to select or exclude multiple event types.
+ master_event_id: Filter for instances of recurring events with the given
+ master event ID. Not respected by metadata filtering.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ tentative_as_busy: When set to false, treats tentative calendar events as busy:false.
+ Only applicable for Microsoft and EWS calendar providers. Defaults to true.
+ limit: The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token: An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ """
+
+ calendar_id: str
+ show_cancelled: NotRequired[bool]
+ title: NotRequired[str]
+ description: NotRequired[str]
+ location: NotRequired[str]
+ start: NotRequired[int]
+ end: NotRequired[int]
+ metadata_pair: NotRequired[Dict[str, Any]]
+ expand_recurring: NotRequired[bool]
+ busy: NotRequired[bool]
+ order_by: NotRequired[str]
+ event_type: NotRequired[List[EventType]]
+ master_event_id: NotRequired[str]
+ select: NotRequired[str]
+ tentative_as_busy: NotRequired[bool]
+
+
+class CreateEventQueryParams(TypedDict):
+ """
+ Interface representing of the query parameters for creating an event.
+
+ Attributes:
+ calendar_id: The ID of the calendar to create the event in.
+ notify_participants: Email notifications containing the calendar event is sent to all event participants.
+ tentative_as_busy: When set to false, treats tentative calendar events as busy:false.
+ Only applicable for Microsoft and EWS calendar providers. Defaults to true.
+ """
+
+ calendar_id: str
+ notify_participants: NotRequired[bool]
+ tentative_as_busy: NotRequired[bool]
+
+
+class FindEventQueryParams(TypedDict):
+ """
+ Interface representing of the query parameters for finding an event.
+
+ Attributes:
+ calendar_id: Calendar ID to find the event in.
+ "primary" is a supported value indicating the user's primary calendar.
+ tentative_as_busy: When set to false, treats tentative calendar events as busy:false.
+ Only applicable for Microsoft and EWS calendar providers. Defaults to true.
+ """
+
+ calendar_id: str
+ tentative_as_busy: NotRequired[bool]
+
+
+UpdateEventQueryParams = CreateEventQueryParams
+""" Interface representing of the query parameters for updating an Event. """
+
+DestroyEventQueryParams = CreateEventQueryParams
+""" Interface representing of the query parameters for destroying an Event. """
+
+
+class SendRsvpQueryParams(TypedDict):
+ """
+ Interface representing of the query parameters for an event.
+
+ Attributes:
+ calendar_id: Calendar ID to find the event in.
+ "primary" is a supported value indicating the user's primary calendar.
+ """
+
+ calendar_id: str
+
+
+class SendRsvpRequest(TypedDict):
+ """
+ Interface representing a request to send an RSVP.
+
+ Attributes:
+ status: The status of the RSVP.
+ """
+
+ status: SendRsvpStatus
+
+
+class ListImportEventsQueryParams(ListQueryParams):
+ """
+ Interface representing the query parameters for listing imported events.
+
+ Attributes:
+ calendar_id: Specify calendar ID to import events to. "primary" is a supported value
+ indicating the user's primary calendar.
+ start: Filter for events that start at or after the specified time, in Unix timestamp format.
+ end: Filter for events that end at or before the specified time, in Unix timestamp format.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ page_token: An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ """
+
+ calendar_id: str
+ start: NotRequired[int]
+ end: NotRequired[int]
+ select: NotRequired[str]
+ page_token: NotRequired[str]
diff --git a/nylas/models/folders.py b/nylas/models/folders.py
new file mode 100644
index 00000000..5eac55f3
--- /dev/null
+++ b/nylas/models/folders.py
@@ -0,0 +1,113 @@
+from dataclasses import dataclass
+from typing import Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+@dataclass_json
+@dataclass
+class Folder:
+ """
+ Class representing a Nylas folder.
+
+ Attributes:
+ id: A globally unique object identifier.
+ grant_id: A Grant ID of the Nylas account.
+ name: Folder name
+ object: The type of object.
+ parent_id: ID of the parent folder. (Microsoft only)
+ background_color: Folder background color. (Google only)
+ text_color: Folder text color. (Google only)
+ system_folder: Indicates if the folder is user created or system created. (Google Only)
+ child_count: The number of immediate child folders in the current folder. (Microsoft only)
+ unread_count: The number of unread items inside of a folder.
+ total_count: The number of items inside of a folder.
+ attributes: Common attribute descriptors shared by system folders across providers.
+ For example, Sent email folders have the `["\\Sent"]` attribute.
+ For IMAP grants, IMAP providers provide the attributes.
+ For Google and Microsoft Graph, Nylas matches system folders to a set of common attributes.
+ """
+
+ id: str
+ grant_id: str
+ name: str
+ object: str = "folder"
+ parent_id: Optional[str] = None
+ background_color: Optional[str] = None
+ text_color: Optional[str] = None
+ system_folder: Optional[bool] = None
+ child_count: Optional[int] = None
+ unread_count: Optional[int] = None
+ total_count: Optional[int] = None
+ attributes: Optional[str] = None
+
+
+class CreateFolderRequest(TypedDict):
+ """
+ Class representation of the Nylas folder creation request.
+
+ Attributes:
+ name: The name of the folder.
+ parent_id: The parent ID of the folder. (Microsoft only)
+ background_color: The background color of the folder. (Google only)
+ text_color: The text color of the folder. (Google only)
+ """
+
+ name: str
+ parent_id: NotRequired[str]
+ background_color: NotRequired[str]
+ text_color: NotRequired[str]
+
+
+class UpdateFolderRequest(TypedDict):
+ """
+ Class representation of the Nylas folder update request.
+
+ Attributes:
+ name: The name of the folder.
+ parent_id: The parent ID of the folder. (Microsoft only)
+ background_color: The background color of the folder. (Google only)
+ text_color: The text color of the folder. (Google only)
+ """
+
+ name: NotRequired[str]
+ parent_id: NotRequired[str]
+ background_color: NotRequired[str]
+ text_color: NotRequired[str]
+
+
+class ListFolderQueryParams(ListQueryParams):
+ """
+ Interface representing the query parameters for listing folders.
+
+ Attributes:
+ parent_id: (Microsoft and EWS only.) Use the ID of a folder to find all child folders it contains.
+ include_hidden_folders: (Microsoft only) When true, Nylas includes hidden folders in its response.
+ single_level: (Microsoft only) If true, retrieves folders from a single-level hierarchy only.
+ If false, retrieves folders across a multi-level hierarchy. Defaults to false.
+ select (NotRequired[str]): Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ """
+
+ parent_id: NotRequired[str]
+ include_hidden_folders: NotRequired[bool]
+ single_level: NotRequired[bool]
+
+
+class FindFolderQueryParams(TypedDict):
+ """
+ Interface representing the query parameters for finding a folder.
+
+ Attributes:
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ select: NotRequired[str]
diff --git a/nylas/models/free_busy.py b/nylas/models/free_busy.py
new file mode 100644
index 00000000..680ddd2e
--- /dev/null
+++ b/nylas/models/free_busy.py
@@ -0,0 +1,71 @@
+from dataclasses import dataclass
+from typing import List, Union
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict
+
+
+@dataclass_json
+@dataclass
+class FreeBusyError:
+ """
+ Interface for a Nylas free/busy call error
+
+ Attributes:
+ email: The email address of the participant who had an error.
+ error: The provider's error message.
+ """
+
+ email: str
+ error: str
+
+
+@dataclass_json
+@dataclass
+class TimeSlot:
+ """
+ Interface for a Nylas free/busy time slot
+
+ Attributes:
+ start_time: Unix timestamp for the start of the slot.
+ end_time: Unix timestamp for the end of the slot.
+ status: The status of the slot. Typically "busy"
+ """
+
+ start_time: int
+ end_time: int
+ status: str
+
+
+@dataclass_json
+@dataclass
+class FreeBusy:
+ """
+ Interface for an individual Nylas free/busy response
+
+ Attributes:
+ email: The email address of the participant.
+ time_slots: List of time slots for the participant.
+ """
+
+ email: str
+ time_slots: List[TimeSlot]
+
+
+GetFreeBusyResponse = List[Union[FreeBusy, FreeBusyError]]
+""" Interface for a Nylas get free/busy response """
+
+
+class GetFreeBusyRequest(TypedDict):
+ """
+ Interface for a Nylas get free/busy request
+
+ Attributes:
+ start_time: Unix timestamp for the start time to check free/busy for.
+ end_time: Unix timestamp for the end time to check free/busy for.
+ emails: List of email addresses to check free/busy for.
+ """
+
+ start_time: int
+ end_time: int
+ emails: List[str]
diff --git a/nylas/models/grants.py b/nylas/models/grants.py
new file mode 100644
index 00000000..7d793a21
--- /dev/null
+++ b/nylas/models/grants.py
@@ -0,0 +1,112 @@
+from dataclasses import dataclass, field
+from typing import List, Any, Dict, Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+from nylas.models.auth import Provider
+
+
+@dataclass_json
+@dataclass
+class Grant:
+ """
+ Interface representing a Nylas Grant object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ provider: OAuth provider that the user authenticated with.
+ account_id: Globally unique identifier for your v2 account that has been migrated using our migration APIs.
+ scope: Scopes specified for the grant.
+ created_at: Unix timestamp when the grant was created.
+ grant_status: Status of the grant, if it is still valid or if the user needs to re-authenticate.
+ email: Email address associated with the grant.
+ user_agent: End user's client user agent.
+ ip: End user's client IP address.
+ state: Initial state that was sent as part of the OAuth request.
+ updated_at: Unix timestamp when the grant was updated.
+ provider_user_id: Provider's ID for the user this grant is associated with.
+ settings: Settings required by the provider that were sent as part of the OAuth request.
+ """
+
+ id: str
+ provider: str
+ scope: List[str] = field(default_factory=list)
+ account_id: Optional[str] = None
+ grant_status: Optional[str] = None
+ email: Optional[str] = None
+ user_agent: Optional[str] = None
+ ip: Optional[str] = None
+ state: Optional[str] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+ provider_user_id: Optional[str] = None
+ settings: Optional[Dict[str, Any]] = None
+ credential_id: Optional[str] = None
+
+
+class CreateGrantRequest(TypedDict):
+ """
+ Interface representing a request to create a grant.
+
+ Attributes:
+ provider: OAuth provider.
+ settings: Settings required by provider.
+ state: Optional state value to return to developer's website after authentication flow is completed.
+ scope: Optional list of scopes to request. If not specified it will use the integration default scopes.
+ """
+
+ provider: Provider
+ settings: Dict[str, Any]
+ state: NotRequired[str]
+ scope: NotRequired[List[str]]
+
+
+class UpdateGrantRequest(TypedDict):
+ """
+ Interface representing a request to update a grant.
+
+ Attributes:
+ settings: Settings required by provider.
+ scope: List of integration scopes for the grant.
+ """
+
+ settings: NotRequired[Dict[str, Any]]
+ scope: NotRequired[List[str]]
+
+
+class ListGrantsQueryParams(TypedDict):
+ """
+ Interface representing the query parameters for listing grants.
+
+ Attributes:
+ limit: The maximum number of objects to return.
+ This field defaults to 10. The maximum allowed value is 200.
+ offset: Offset grant results by this number.
+ sort_by: Sort entries by field name.
+ order_by: Specify ascending or descending order.
+ since: Scope grants from a specific point in time by Unix timestamp.
+ before: Scope grants to a specific point in time by Unix timestamp.
+ email: Filtering your query based on grant email address (if applicable)
+ grant_status: Filtering your query based on grant email status (if applicable)
+ ip: Filtering your query based on grant IP address
+ provider: Filtering your query based on OAuth provider
+ sortBy: Deprecated camelCase alias for sort_by.
+ orderBy: Deprecated camelCase alias for order_by.
+ grantStatus: Deprecated camelCase alias for grant_status.
+ """
+
+ limit: NotRequired[int]
+ offset: NotRequired[int]
+ sort_by: NotRequired[str]
+ order_by: NotRequired[str]
+ since: NotRequired[int]
+ before: NotRequired[int]
+ email: NotRequired[str]
+ grant_status: NotRequired[str]
+ ip: NotRequired[str]
+ provider: NotRequired[Provider]
+ # Backward-compatible aliases for callers still passing camelCase keys.
+ sortBy: NotRequired[str]
+ orderBy: NotRequired[str]
+ grantStatus: NotRequired[str]
diff --git a/nylas/models/list_query_params.py b/nylas/models/list_query_params.py
new file mode 100644
index 00000000..6ac6b645
--- /dev/null
+++ b/nylas/models/list_query_params.py
@@ -0,0 +1,19 @@
+from typing_extensions import TypedDict, NotRequired
+
+
+class ListQueryParams(TypedDict):
+ """
+ Interface of the query parameters for listing resources.
+
+ Attributes:
+ limit: The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token: An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ limit: NotRequired[int]
+ page_token: NotRequired[str]
+ select: NotRequired[str]
diff --git a/nylas/models/lists.py b/nylas/models/lists.py
new file mode 100644
index 00000000..c4732e61
--- /dev/null
+++ b/nylas/models/lists.py
@@ -0,0 +1,69 @@
+from dataclasses import dataclass
+from typing import List as TypingList, Literal, Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import NotRequired, TypedDict
+
+from nylas.models.list_query_params import ListQueryParams
+
+ListType = Literal["domain", "tld", "address"]
+
+
+class ListListsQueryParams(ListQueryParams):
+ """Query parameters for listing lists."""
+
+ pass
+
+
+class ListListItemsQueryParams(ListQueryParams):
+ """Query parameters for listing items in a list."""
+
+ pass
+
+
+class CreateListRequest(TypedDict):
+ """Request body for creating a list."""
+
+ name: str
+ type: ListType
+ description: NotRequired[str]
+
+
+class UpdateListRequest(TypedDict, total=False):
+ """Request body for updating a list."""
+
+ name: NotRequired[str]
+ description: NotRequired[str]
+
+
+class UpdateListItemsRequest(TypedDict):
+ """Request body for adding/removing list items."""
+
+ items: TypingList[str]
+
+
+@dataclass_json
+@dataclass
+class NylasList:
+ """A typed collection used in `in_list` rule conditions."""
+
+ id: Optional[str] = None
+ name: Optional[str] = None
+ description: Optional[str] = None
+ type: Optional[str] = None
+ items_count: Optional[int] = None
+ application_id: Optional[str] = None
+ organization_id: Optional[str] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class ListItem:
+ """A single value belonging to a Nylas list."""
+
+ id: Optional[str] = None
+ list_id: Optional[str] = None
+ value: Optional[str] = None
+ created_at: Optional[int] = None
diff --git a/nylas/models/messages.py b/nylas/models/messages.py
new file mode 100644
index 00000000..9caf5bfc
--- /dev/null
+++ b/nylas/models/messages.py
@@ -0,0 +1,298 @@
+from dataclasses import dataclass, field
+from typing import List, Literal, Optional, Dict, Any
+from dataclasses_json import dataclass_json, config
+from typing_extensions import TypedDict, NotRequired, get_type_hints
+
+from nylas.models.attachments import Attachment
+from nylas.models.list_query_params import ListQueryParams
+from nylas.models.events import EmailName
+
+
+Fields = Literal["standard", "include_headers", "include_tracking_options", "raw_mime"]
+""" Literal representing which headers to include with a message. """
+
+
+@dataclass_json
+@dataclass
+class MessageHeader:
+ """
+ A message header.
+
+ Attributes:
+ name: The header name.
+ value: The header value.
+ """
+
+ name: str
+ value: str
+
+
+@dataclass_json
+@dataclass
+class TrackingOptions:
+ """
+ Message tracking options.
+
+ Attributes:
+ opens: When true, shows that message open tracking is enabled.
+ thread_replies: When true, shows that thread replied tracking is enabled.
+ links: When true, shows that link clicked tracking is enabled.
+ label: A label describing the message tracking purpose.
+ """
+
+ opens: Optional[bool] = None
+ thread_replies: Optional[bool] = None
+ links: Optional[bool] = None
+ label: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class Message:
+ """
+ A Message object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: The grant that this message belongs to.
+ thread_id: The thread that this message belongs to.
+ subject: The subject of the message.
+ from_: The sender of the message.
+ object: The type of object.
+ to: The recipients of the message.
+ cc: The CC recipients of the message.
+ bcc: The BCC recipients of the message.
+ reply_to: The reply-to recipients of the message.
+ date: The date the message was received.
+ unread: Whether the message is unread.
+ starred: Whether the message is starred.
+ snippet: A snippet of the message body.
+ body: The body of the message.
+ attachments: The attachments on the message.
+ folders: The folders that the message is in.
+ headers: The headers of the message.
+ created_at: Unix timestamp of when the message was created.
+ schedule_id: The ID of the scheduled email message. Nylas returns the schedule_id if send_at is set.
+ send_at: Unix timestamp of when the message will be sent, if scheduled.
+ tracking_options: The tracking options for the message.
+ raw_mime: A Base64url-encoded string containing the message data (including the body content).
+ """
+
+ grant_id: str
+ from_: Optional[List[EmailName]] = field(
+ default=None, metadata=config(field_name="from")
+ )
+ object: str = "message"
+ id: Optional[str] = None
+ body: Optional[str] = None
+ thread_id: Optional[str] = None
+ subject: Optional[str] = None
+ snippet: Optional[str] = None
+ to: Optional[List[EmailName]] = None
+ bcc: Optional[List[EmailName]] = None
+ cc: Optional[List[EmailName]] = None
+ reply_to: Optional[List[EmailName]] = None
+ attachments: Optional[List[Attachment]] = None
+ folders: Optional[List[str]] = None
+ headers: Optional[List[MessageHeader]] = None
+ unread: Optional[bool] = None
+ starred: Optional[bool] = None
+ created_at: Optional[int] = None
+ date: Optional[int] = None
+ schedule_id: Optional[str] = None
+ send_at: Optional[int] = None
+ metadata: Optional[Dict[str, Any]] = None
+ tracking_options: Optional[TrackingOptions] = None
+ raw_mime: Optional[str] = None
+
+
+# Need to use Functional typed dicts because "from" and "in" are Python
+# keywords, and can't be declared using the declarative syntax
+ListMessagesQueryParams = TypedDict(
+ "ListMessagesQueryParams",
+ {
+ **get_type_hints(ListQueryParams), # Inherit fields from ListQueryParams
+ "subject": NotRequired[str],
+ "any_email": NotRequired[List[str]],
+ "from": NotRequired[List[str]],
+ "to": NotRequired[List[str]],
+ "cc": NotRequired[List[str]],
+ "bcc": NotRequired[List[str]],
+ "in": NotRequired[str],
+ "unread": NotRequired[bool],
+ "starred": NotRequired[bool],
+ "thread_id": NotRequired[str],
+ "received_before": NotRequired[int],
+ "received_after": NotRequired[int],
+ "has_attachment": NotRequired[bool],
+ "fields": NotRequired[Fields],
+ "search_query_native": NotRequired[str],
+ "select": NotRequired[str],
+ "metadata_pair": NotRequired[str]
+ },
+)
+"""
+Query parameters for listing messages.
+
+Attributes:
+ subject: Return messages with matching subject.
+ any_email: Return messages that have been sent or received by this comma-separated list of email addresses.
+ from: Return messages sent from this email address.
+ to: Return messages sent to this email address.
+ cc: Return messages cc'd to this email address.
+ bcc: Return messages bcc'd to this email address.
+ in: Return messages in this specific folder or label, specified by ID.
+ unread: Filter messages by unread status.
+ starred: Filter messages by starred status.
+ thread_id: Filter messages by thread_id.
+ received_before: Return messages with received dates before received_before.
+ received_after: Return messages with received dates after received_after.
+ has_attachment: Filter messages by whether they have an attachment.
+ fields: Specify which headers to include in the response.
+ - "standard" (default): Returns the standard message payload.
+ - "include_headers": Returns messages and their custom headers.
+ - "include_tracking_options": Returns messages and their tracking settings.
+ - "raw_mime": Returns the grant_id, object, id, and raw_mime fields for each message.
+ search_query_native: A native provider search query for Google or Microsoft.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ metadata_pair (NotRequired[str]): Pass a metadata key/value pair (for example, ?metadata_pair=key1:value)
+ to search for metadata associated with objects. See Metadata for more information.
+"""
+
+
+class FindMessageQueryParams(TypedDict):
+ """
+ Query parameters for finding a message.
+
+ Attributes:
+ fields: Specify which headers to include in the response.
+ - "standard" (default): Returns the standard message payload.
+ - "include_headers": Returns messages and their custom headers.
+ - "include_tracking_options": Returns messages and their tracking settings.
+ - "raw_mime": Returns the grant_id, object, id, and raw_mime fields for each message.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ """
+
+ fields: NotRequired[Fields]
+ select: NotRequired[str]
+
+
+class UpdateMessageRequest(TypedDict):
+ """
+ Request payload for updating a message.
+
+ Attributes:
+ starred: The message's starred status
+ unread: The message's unread status
+ folders: The message's folders
+ metadata: A list of key-value pairs storing additional data
+ """
+
+ unread: NotRequired[bool]
+ starred: NotRequired[bool]
+ folders: NotRequired[List[str]]
+ metadata: NotRequired[Dict[str, Any]]
+
+
+@dataclass_json
+@dataclass
+class ScheduledMessageStatus:
+ """
+ The status of a scheduled message.
+
+ Attributes:
+ code: The status code the describes the state of the scheduled message.
+ description: A description of the status of the scheduled message.
+ """
+
+ code: str
+ description: str
+
+
+@dataclass_json
+@dataclass
+class ScheduledMessage:
+ """
+ A scheduled message.
+
+ Attributes:
+ schedule_id: The unique identifier for the scheduled message.
+ status: The status of the scheduled message.
+ close_time: The time the message was sent or failed to send, in epoch time.
+ """
+
+ schedule_id: str
+ status: ScheduledMessageStatus
+ close_time: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class StopScheduledMessageResponse:
+ """
+ The response from stopping a scheduled message.
+
+ Attributes:
+ message: A message describing the result of the request.
+ """
+
+ message: str
+
+
+class CleanMessagesRequest(TypedDict):
+ """
+ Request to clean a list of messages.
+
+ Attributes:
+ message_id: IDs of the email messages to clean.
+ ignore_links: If true, removes link-related tags () from the email message while keeping the text.
+ ignore_images: If true, removes images from the email message.
+ images_as_markdown: If true, converts images in the email message to Markdown.
+ ignore_tables: If true, removes table-related tags (
,
,
,
) from the email message while
+ keeping rows.
+ remove_conclusion_phrases: If true, removes phrases such as "Best" and "Regards" in the email message signature.
+ """
+
+ message_id: List[str]
+ ignore_links: NotRequired[bool]
+ ignore_images: NotRequired[bool]
+ images_as_markdown: NotRequired[bool]
+ ignore_tables: NotRequired[bool]
+ remove_conclusion_phrases: NotRequired[bool]
+
+
+@dataclass_json
+@dataclass
+class CleanMessagesResponse(Message):
+ """
+ Message object with the cleaned HTML message body.
+
+ Attributes:
+ id (str): Globally unique object identifier.
+ grant_id (str): The grant that this message belongs to.
+ from_ (List[EmailName]): The sender of the message.
+ date (int): The date the message was received.
+ object: The type of object.
+ thread_id (Optional[str]): The thread that this message belongs to.
+ subject (Optional[str]): The subject of the message.
+ to (Optional[List[EmailName]]): The recipients of the message.
+ cc (Optional[List[EmailName]]): The CC recipients of the message.
+ bcc (Optional[List[EmailName]]): The BCC recipients of the message.
+ reply_to (Optional[List[EmailName]]): The reply-to recipients of the message.
+ unread (Optional[bool]): Whether the message is unread.
+ starred (Optional[bool]): Whether the message is starred.
+ snippet (Optional[str]): A snippet of the message body.
+ body (Optional[str]): The body of the message.
+ attachments (Optional[List[Attachment]]): The attachments on the message.
+ folders (Optional[List[str]]): The folders that the message is in.
+ created_at (Optional[int]): Unix timestamp of when the message was created.
+ conversation (str): The cleaned HTML message body.
+ """
+
+ conversation: str = ""
diff --git a/nylas/models/notetakers.py b/nylas/models/notetakers.py
new file mode 100644
index 00000000..17badf6e
--- /dev/null
+++ b/nylas/models/notetakers.py
@@ -0,0 +1,309 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import NotRequired, TypedDict
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+class NotetakerState(str, Enum):
+ """
+ Enum representing the possible states of a Notetaker bot.
+
+ Values:
+ SCHEDULED: The Notetaker is scheduled to join a meeting.
+ CONNECTING: The Notetaker is connecting to the meeting.
+ WAITING_FOR_ENTRY: The Notetaker is waiting to be admitted to the meeting.
+ FAILED_ENTRY: The Notetaker failed to join the meeting.
+ ATTENDING: The Notetaker is currently in the meeting.
+ MEDIA_PROCESSING: The Notetaker is processing media from the meeting.
+ MEDIA_AVAILABLE: The Notetaker has processed media available for download.
+ MEDIA_ERROR: An error occurred while processing the media.
+ MEDIA_DELETED: The meeting media has been deleted.
+ """
+
+ SCHEDULED = "scheduled"
+ CONNECTING = "connecting"
+ WAITING_FOR_ENTRY = "waiting_for_entry"
+ FAILED_ENTRY = "failed_entry"
+ ATTENDING = "attending"
+ MEDIA_PROCESSING = "media_processing"
+ MEDIA_AVAILABLE = "media_available"
+ MEDIA_ERROR = "media_error"
+ MEDIA_DELETED = "media_deleted"
+
+
+class NotetakerOrderBy(str, Enum):
+ """
+ Enum representing the possible fields to order Notetaker bots by.
+
+ Values:
+ NAME: Order by the Notetaker's name.
+ JOIN_TIME: Order by the Notetaker's join time.
+ CREATED_AT: Order by when the Notetaker was created.
+ """
+
+ NAME = "name"
+ JOIN_TIME = "join_time"
+ CREATED_AT = "created_at"
+
+
+class NotetakerOrderDirection(str, Enum):
+ """
+ Enum representing the possible directions to order Notetaker bots by.
+
+ Values:
+ ASC: Ascending order.
+ DESC: Descending order.
+ """
+
+ ASC = "asc"
+ DESC = "desc"
+
+
+class MeetingProvider(str, Enum):
+ """
+ Enum representing the possible meeting providers for Notetaker.
+
+ Values:
+ GOOGLE_MEET: Google Meet meetings
+ ZOOM: Zoom meetings
+ MICROSOFT_TEAMS: Microsoft Teams meetings
+ """
+
+ GOOGLE_MEET = "Google Meet"
+ ZOOM = "Zoom Meeting"
+ MICROSOFT_TEAMS = "Microsoft Teams"
+
+
+class NotetakerMeetingSettingsRequest(TypedDict):
+ """
+ Interface representing Notetaker meeting settings for request objects.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ If transcription is true, audio_recording must also be true.
+ """
+
+ video_recording: Optional[bool]
+ audio_recording: Optional[bool]
+ transcription: Optional[bool]
+
+
+@dataclass_json
+@dataclass
+class NotetakerMeetingSettings:
+ """
+ Class representing Notetaker meeting settings.
+
+ Attributes:
+ video_recording: When true, Notetaker records the meeting's video.
+ audio_recording: When true, Notetaker records the meeting's audio.
+ transcription: When true, Notetaker transcribes the meeting's audio.
+ If transcription is true, audio_recording must also be true.
+ """
+
+ video_recording: bool = True
+ audio_recording: bool = True
+ transcription: bool = True
+
+
+@dataclass_json
+@dataclass
+class NotetakerMediaRecording:
+ """
+ Class representing a Notetaker media recording.
+
+ Attributes:
+ size: The size of the file in bytes.
+ name: The name of the file.
+ type: The MIME type of the file.
+ created_at: Unix timestamp when the file was uploaded to the storage server.
+ expires_at: Unix timestamp when the file will be deleted.
+ url: A link to download the file.
+ ttl: Time-to-live in seconds until the file will be deleted off Nylas' storage server.
+ """
+
+ size: int
+ name: str
+ type: str
+ created_at: int
+ expires_at: int
+ url: str
+ ttl: int
+
+
+@dataclass_json
+@dataclass
+class NotetakerMedia:
+ """
+ Class representing Notetaker media.
+
+ Attributes:
+ recording: The meeting recording (video/mp4).
+ transcript: The meeting transcript (application/json).
+ """
+
+ recording: Optional[NotetakerMediaRecording] = None
+ transcript: Optional[NotetakerMediaRecording] = None
+
+
+@dataclass_json
+@dataclass
+class Notetaker:
+ """
+ Class representing a Nylas Notetaker.
+
+ Attributes:
+ id: The Notetaker ID.
+ name: The display name for the Notetaker bot.
+ join_time: When Notetaker joined the meeting, in Unix timestamp format.
+ meeting_link: The meeting link.
+ meeting_provider: The meeting provider.
+ state: The current state of the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ message: A message describing the API response (only included in some responses).
+ """
+
+ id: str
+ name: str
+ join_time: int
+ meeting_link: str
+ state: NotetakerState
+ meeting_settings: NotetakerMeetingSettings
+ meeting_provider: Optional[MeetingProvider] = None
+ message: Optional[str] = None
+ object: str = "notetaker"
+
+ def is_state(self, state: NotetakerState) -> bool:
+ """
+ Check if the notetaker is in a specific state.
+
+ Args:
+ state: The NotetakerState to check against.
+
+ Returns:
+ True if the notetaker is in the specified state, False otherwise.
+ """
+ return self.state == state
+
+ def is_scheduled(self) -> bool:
+ """Check if the notetaker is in the scheduled state."""
+ return self.is_state(NotetakerState.SCHEDULED)
+
+ def is_attending(self) -> bool:
+ """Check if the notetaker is currently attending a meeting."""
+ return self.is_state(NotetakerState.ATTENDING)
+
+ def has_media_available(self) -> bool:
+ """Check if the notetaker has media available for download."""
+ return self.is_state(NotetakerState.MEDIA_AVAILABLE)
+
+
+class InviteNotetakerRequest(TypedDict):
+ """
+ Interface representing the Nylas notetaker creation request.
+
+ Attributes:
+ meeting_link: A meeting invitation link that Notetaker uses to join the meeting.
+ join_time: When Notetaker should join the meeting, in Unix timestamp format.
+ If empty, Notetaker joins the meeting immediately.
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ """
+
+ meeting_link: str
+ join_time: NotRequired[int]
+ name: NotRequired[str]
+ meeting_settings: NotRequired[NotetakerMeetingSettingsRequest]
+
+
+class UpdateNotetakerRequest(TypedDict):
+ """
+ Interface representing the Nylas notetaker update request.
+
+ Attributes:
+ join_time: When Notetaker should join the meeting, in Unix timestamp format.
+ name: The display name for the Notetaker bot.
+ meeting_settings: Notetaker Meeting Settings.
+ """
+
+ join_time: NotRequired[int]
+ name: NotRequired[str]
+ meeting_settings: NotRequired[NotetakerMeetingSettingsRequest]
+
+
+class ListNotetakerQueryParams(ListQueryParams):
+ """
+ Interface representing the query parameters for listing notetakers.
+
+ Attributes:
+ state: Filter for Notetaker bots with the specified meeting state.
+ Use the NotetakerState enum.
+ Example: state=NotetakerState.SCHEDULED
+ join_time_start: Filter for Notetaker bots that have join times that start at or after a specific time,
+ in Unix timestamp format.
+ join_time_end: Filter for Notetaker bots that have join times that end at or are before a specific time,
+ in Unix timestamp format.
+ limit: The maximum number of objects to return. This field defaults to 50. The maximum allowed value is 200.
+ page_token: An identifier that specifies which page of data to return.
+ prev_page_token: An identifier that specifies which page of data to return.
+ order_by: The field to order the Notetaker bots by. Defaults to created_at.
+ Use the NotetakerOrderBy enum.
+ Example: order_by=NotetakerOrderBy.NAME
+ order_direction: The direction to order the Notetaker bots by. Defaults to asc.
+ Use the NotetakerOrderDirection enum.
+ Example: order_direction=NotetakerOrderDirection.DESC
+ """
+
+ state: NotRequired[NotetakerState]
+ join_time_start: NotRequired[int]
+ join_time_end: NotRequired[int]
+ order_by: NotRequired[NotetakerOrderBy]
+ order_direction: NotRequired[NotetakerOrderDirection]
+
+ def __post_init__(self):
+ """Convert enums to string values for API requests."""
+ # Convert state enum to string if present
+ if hasattr(self, "state") and isinstance(self.state, NotetakerState):
+ self.state = self.state.value
+ # Convert order_by enum to string if present
+ if hasattr(self, "order_by") and isinstance(self.order_by, NotetakerOrderBy):
+ self.order_by = self.order_by.value
+ # Convert order_direction enum to string if present
+ if hasattr(self, "order_direction") and isinstance(
+ self.order_direction, NotetakerOrderDirection
+ ):
+ self.order_direction = self.order_direction.value
+
+
+class FindNotetakerQueryParams(TypedDict):
+ """
+ Interface representing the query parameters for finding a notetaker.
+
+ Attributes:
+ select: Comma-separated list of fields to return in the response.
+ Use this to limit the fields returned in the response.
+ """
+
+ select: NotRequired[str]
+
+
+@dataclass_json
+@dataclass
+class NotetakerLeaveResponse:
+ """
+ Class representing a Notetaker leave response.
+
+ Attributes:
+ id: The Notetaker ID.
+ message: A message describing the API response.
+ """
+
+ id: str
+ message: str
+ object: str = "notetaker_leave_response"
diff --git a/nylas/models/policies.py b/nylas/models/policies.py
new file mode 100644
index 00000000..48dbafef
--- /dev/null
+++ b/nylas/models/policies.py
@@ -0,0 +1,118 @@
+from dataclasses import dataclass
+from typing import List, Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import NotRequired, TypedDict
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+class ListPoliciesQueryParams(ListQueryParams):
+ """
+ Query parameters for listing policies.
+
+ Attributes:
+ limit: Maximum number of objects to return.
+ page_token: Cursor for the next page (from ``next_cursor`` on the previous response).
+ """
+
+ pass
+
+
+class PolicyOptionsRequest(TypedDict, total=False):
+ """Request shape for policy options."""
+
+ additional_folders: NotRequired[List[str]]
+ use_cidr_aliasing: NotRequired[bool]
+
+
+class PolicyLimitsRequest(TypedDict, total=False):
+ """Request shape for policy limits."""
+
+ limit_attachment_size_limit: NotRequired[int]
+ limit_attachment_count_limit: NotRequired[int]
+ limit_attachment_allowed_types: NotRequired[List[str]]
+ limit_size_total_mime: NotRequired[int]
+ limit_storage_total: NotRequired[int]
+ limit_count_daily_message_per_grant: NotRequired[int]
+ limit_inbox_retention_period: NotRequired[int]
+ limit_spam_retention_period: NotRequired[int]
+
+
+class PolicySpamDetectionRequest(TypedDict, total=False):
+ """Request shape for policy spam detection settings."""
+
+ use_list_dnsbl: NotRequired[bool]
+ use_header_anomaly_detection: NotRequired[bool]
+ spam_sensitivity: NotRequired[float]
+
+
+class CreatePolicyRequest(TypedDict):
+ """Request body for creating a policy."""
+
+ name: str
+ options: NotRequired[PolicyOptionsRequest]
+ limits: NotRequired[PolicyLimitsRequest]
+ rules: NotRequired[List[str]]
+ spam_detection: NotRequired[PolicySpamDetectionRequest]
+
+
+class UpdatePolicyRequest(TypedDict, total=False):
+ """Request body for updating a policy."""
+
+ name: NotRequired[str]
+ options: NotRequired[PolicyOptionsRequest]
+ limits: NotRequired[PolicyLimitsRequest]
+ rules: NotRequired[List[str]]
+ spam_detection: NotRequired[PolicySpamDetectionRequest]
+
+
+@dataclass_json
+@dataclass
+class PolicyOptions:
+ """Policy options applied to inboxes that use this policy."""
+
+ additional_folders: Optional[List[str]] = None
+ use_cidr_aliasing: Optional[bool] = None
+
+
+@dataclass_json
+@dataclass
+class PolicyLimits:
+ """Operational limits applied to inboxes that use this policy."""
+
+ limit_attachment_size_limit: Optional[int] = None
+ limit_attachment_count_limit: Optional[int] = None
+ limit_attachment_allowed_types: Optional[List[str]] = None
+ limit_size_total_mime: Optional[int] = None
+ limit_storage_total: Optional[int] = None
+ limit_count_daily_message_per_grant: Optional[int] = None
+ limit_inbox_retention_period: Optional[int] = None
+ limit_spam_retention_period: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class PolicySpamDetection:
+ """Spam detection settings applied to inboxes that use this policy."""
+
+ use_list_dnsbl: Optional[bool] = None
+ use_header_anomaly_detection: Optional[bool] = None
+ spam_sensitivity: Optional[float] = None
+
+
+@dataclass_json
+@dataclass
+class Policy:
+ """A policy for Nylas Agent Accounts."""
+
+ id: Optional[str] = None
+ name: Optional[str] = None
+ application_id: Optional[str] = None
+ organization_id: Optional[str] = None
+ options: Optional[PolicyOptions] = None
+ limits: Optional[PolicyLimits] = None
+ rules: Optional[List[str]] = None
+ spam_detection: Optional[PolicySpamDetection] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
diff --git a/nylas/models/redirect_uri.py b/nylas/models/redirect_uri.py
new file mode 100644
index 00000000..21894027
--- /dev/null
+++ b/nylas/models/redirect_uri.py
@@ -0,0 +1,98 @@
+from dataclasses import dataclass
+from typing import Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+
+@dataclass_json
+@dataclass
+class RedirectUriSettings:
+ """
+ Configuration settings for a Redirect URI object.
+
+ Attributes:
+ origin: Related to JS platform.
+ bundle_id: Related to iOS platform.
+ app_store_id: Related to iOS platform.
+ team_id: Related to iOS platform.
+ package_name: Related to Android platform.
+ sha1_certificate_fingerprint: Related to Android platform.
+ """
+
+ origin: Optional[str] = None
+ bundle_id: Optional[str] = None
+ app_store_id: Optional[str] = None
+ team_id: Optional[str] = None
+ package_name: Optional[str] = None
+ sha1_certificate_fingerprint: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class RedirectUri:
+ """
+ Class representing a Redirect URI object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ url: Redirect URL.
+ platform: Platform identifier.
+ settings: Configuration settings.
+ """
+
+ id: str
+ url: str
+ platform: str
+ settings: Optional[RedirectUriSettings] = None
+
+
+class WritableRedirectUriSettings(TypedDict):
+ """
+ Class representing redirect uri settings to be provided for a create/update call.
+
+ Attributes:
+ origin: Optional origin for the redirect uri.
+ bundle_id: Optional bundle id for the redirect uri.
+ app_store_id: Optional app store id for the redirect uri.
+ team_id: Optional team id for the redirect uri.
+ package_name: Optional package name for the redirect uri.
+ sha1_certificate_fingerprint: Optional sha1 certificate fingerprint for the redirect uri.
+ """
+
+ origin: NotRequired[str]
+ bundle_id: NotRequired[str]
+ app_store_id: NotRequired[str]
+ team_id: NotRequired[str]
+ package_name: NotRequired[str]
+ sha1_certificate_fingerprint: NotRequired[str]
+
+
+class CreateRedirectUriRequest(TypedDict):
+ """
+ Class representing a request to create a redirect uri.
+
+ Attributes:
+ url: Redirect URL.
+ platform: Platform identifier.
+ settings: Optional settings for the redirect uri.
+ """
+
+ url: str
+ platform: str
+ settings: NotRequired[WritableRedirectUriSettings]
+
+
+class UpdateRedirectUriRequest(TypedDict):
+ """
+ Class representing a request to update a redirect uri.
+
+ Attributes:
+ url: Redirect URL.
+ platform: Platform identifier.
+ settings: Optional settings for the redirect uri.
+ """
+
+ url: NotRequired[str]
+ platform: NotRequired[str]
+ settings: NotRequired[WritableRedirectUriSettings]
diff --git a/nylas/models/response.py b/nylas/models/response.py
new file mode 100644
index 00000000..64b03d0d
--- /dev/null
+++ b/nylas/models/response.py
@@ -0,0 +1,180 @@
+from dataclasses import dataclass
+from typing import TypeVar, Generic, Optional, List
+
+from dataclasses_json import DataClassJsonMixin
+
+from requests.structures import CaseInsensitiveDict
+
+T = TypeVar("T", bound=DataClassJsonMixin)
+
+
+class Response(tuple, Generic[T]):
+ """
+ Response object returned from the Nylas API.
+
+ Attributes:
+ data: The requested data object.
+ request_id: The request ID.
+ """
+
+ data: T
+ request_id: str
+ headers: Optional[CaseInsensitiveDict] = None
+
+ def __new__(cls, data: T, request_id: str, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Initialize the response object.
+
+ Args:
+ data: The requested data object.
+ request_id: The request ID.
+ headers: The headers returned from the API.
+ """
+ # Initialize the tuple for destructuring support
+ instance = super().__new__(cls, (data, request_id, headers))
+
+ instance.data = data
+ instance.request_id = request_id
+ instance.headers = headers
+
+ return instance
+
+ @classmethod
+ def from_dict(cls, resp: dict, generic_type, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Convert a dictionary to a response object.
+
+ Args:
+ resp: The dictionary to convert.
+ generic_type: The type to deserialize the data object into.
+ headers: The headers returned from the API.
+ """
+
+ return cls(
+ data=generic_type.from_dict(resp["data"]),
+ request_id=resp["request_id"],
+ headers=headers,
+ )
+
+
+class ListResponse(tuple, Generic[T]):
+ """
+ List response object returned from the Nylas API.
+
+ Attributes:
+ data: The list of requested data objects.
+ request_id: The request ID.
+ next_cursor: The cursor to use to get the next page of data.
+ headers: The headers returned from the API.
+ """
+
+ data: List[T]
+ request_id: str
+ next_cursor: Optional[str] = None
+ headers: Optional[CaseInsensitiveDict] = None
+
+ def __new__(
+ cls,
+ data: List[T],
+ request_id: str,
+ next_cursor: Optional[str] = None,
+ headers: Optional[CaseInsensitiveDict] = None
+ ):
+ """
+ Initialize the response object.
+
+ Args:
+ data: The list of requested data objects.
+ request_id: The request ID.
+ next_cursor: The cursor to use to get the next page of data.
+ headers: The headers returned from the API.
+ """
+ # Initialize the tuple for destructuring support
+ instance = super().__new__(cls, (data, request_id, next_cursor, headers))
+
+ instance.data = data
+ instance.request_id = request_id
+ instance.next_cursor = next_cursor
+ instance.headers = headers
+
+ return instance
+
+ @classmethod
+ def from_dict(cls, resp: dict, generic_type, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Convert a dictionary to a response object.
+
+ Args:
+ resp: The dictionary to convert.
+ generic_type: The type to deserialize the data objects into.
+ headers: The headers returned from the API.
+ """
+
+ raw_data = resp.get("data", [])
+ if isinstance(raw_data, dict):
+ next_cursor = resp.get("next_cursor", raw_data.get("next_cursor"))
+ data = raw_data.get("items", [])
+ else:
+ next_cursor = resp.get("next_cursor")
+ data = raw_data
+
+ converted_data = []
+ for item in data:
+ converted_data.append(generic_type.from_dict(item, infer_missing=True))
+
+ return cls(
+ data=converted_data,
+ request_id=resp["request_id"],
+ next_cursor=next_cursor,
+ headers=headers,
+ )
+
+
+@dataclass
+class DeleteResponse:
+ """
+ Delete response object returned from the Nylas API.
+
+ Attributes:
+ request_id: The request ID returned from the API.
+ headers: The headers returned from the API.
+ """
+
+ request_id: str
+ headers: Optional[CaseInsensitiveDict] = None
+
+ @classmethod
+ def from_dict(cls, resp: dict, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Convert a dictionary to a response object.
+
+ Args:
+ resp: The dictionary to convert.
+ headers: The headers returned from the API.
+ """
+ return cls(request_id=resp["request_id"], headers=headers)
+
+
+@dataclass
+class RequestIdOnlyResponse:
+ """
+ Response object returned from the Nylas API that only contains a request ID.
+
+ Attributes:
+ request_id: The request ID returned from the API.
+ headers: The headers returned from the API.
+ """
+
+ request_id: str
+ headers: Optional[CaseInsensitiveDict] = None
+
+ @classmethod
+ def from_dict(cls, resp: dict, headers: Optional[CaseInsensitiveDict] = None):
+ """
+ Convert a dictionary to a response object.
+
+ Args:
+ resp: The dictionary to convert.
+ headers: The headers returned from the API.
+ """
+ return cls(request_id=resp["request_id"], headers=headers)
diff --git a/nylas/models/rules.py b/nylas/models/rules.py
new file mode 100644
index 00000000..2299c1f7
--- /dev/null
+++ b/nylas/models/rules.py
@@ -0,0 +1,155 @@
+from dataclasses import dataclass
+from typing import Any, List, Optional
+
+from dataclasses_json import dataclass_json
+from typing_extensions import NotRequired, TypedDict
+
+from nylas.models.list_query_params import ListQueryParams
+
+
+class ListRulesQueryParams(ListQueryParams):
+ """Query parameters for listing rules."""
+
+ pass
+
+
+class ListRuleEvaluationsQueryParams(ListQueryParams):
+ """Query parameters for listing rule evaluations."""
+
+ pass
+
+
+class RuleConditionRequest(TypedDict):
+ """A single condition used in a rule match clause."""
+
+ field: str
+ operator: str
+ value: Any
+
+
+class RuleMatchRequest(TypedDict):
+ """Match clause for create/update rule requests."""
+
+ conditions: List[RuleConditionRequest]
+ operator: NotRequired[str]
+
+
+class RuleActionRequest(TypedDict):
+ """Action object used in create/update rule requests."""
+
+ type: str
+ value: NotRequired[str]
+
+
+class CreateRuleRequest(TypedDict):
+ """Request body for creating a rule."""
+
+ name: str
+ match: RuleMatchRequest
+ actions: List[RuleActionRequest]
+ description: NotRequired[str]
+ priority: NotRequired[int]
+ enabled: NotRequired[bool]
+ trigger: NotRequired[str]
+
+
+class UpdateRuleRequest(TypedDict, total=False):
+ """Request body for updating a rule."""
+
+ name: NotRequired[str]
+ match: NotRequired[RuleMatchRequest]
+ actions: NotRequired[List[RuleActionRequest]]
+ description: NotRequired[str]
+ priority: NotRequired[int]
+ enabled: NotRequired[bool]
+ trigger: NotRequired[str]
+
+
+@dataclass_json
+@dataclass
+class RuleCondition:
+ """A condition in a rule match clause."""
+
+ field: Optional[str] = None
+ operator: Optional[str] = None
+ value: Optional[Any] = None
+
+
+@dataclass_json
+@dataclass
+class RuleMatch:
+ """A rule's condition set and matching strategy."""
+
+ operator: Optional[str] = None
+ conditions: Optional[List[RuleCondition]] = None
+
+
+@dataclass_json
+@dataclass
+class RuleAction:
+ """An action applied when a rule matches."""
+
+ type: Optional[str] = None
+ value: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class Rule:
+ """A rule used for automated filtering and routing."""
+
+ id: Optional[str] = None
+ name: Optional[str] = None
+ description: Optional[str] = None
+ priority: Optional[int] = None
+ enabled: Optional[bool] = None
+ trigger: Optional[str] = None
+ match: Optional[RuleMatch] = None
+ actions: Optional[List[RuleAction]] = None
+ application_id: Optional[str] = None
+ organization_id: Optional[str] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
+
+
+@dataclass_json
+@dataclass
+class RuleEvaluationInput:
+ """Sender data used as input to rule evaluation."""
+
+ from_address: Optional[str] = None
+ from_domain: Optional[str] = None
+ from_tld: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class RuleEvaluationAppliedActions:
+ """Actions applied when rules matched."""
+
+ blocked: Optional[bool] = None
+ marked_as_spam: Optional[bool] = None
+ marked_as_read: Optional[bool] = None
+ marked_starred: Optional[bool] = None
+ archived: Optional[bool] = None
+ trashed: Optional[bool] = None
+ folder_ids: Optional[List[str]] = None
+
+
+@dataclass_json
+@dataclass
+class RuleEvaluation:
+ """An audit record describing rule evaluation for a grant."""
+
+ id: Optional[str] = None
+ grant_id: Optional[str] = None
+ message_id: Optional[str] = None
+ evaluated_at: Optional[int] = None
+ evaluation_stage: Optional[str] = None
+ evaluation_input: Optional[RuleEvaluationInput] = None
+ applied_actions: Optional[RuleEvaluationAppliedActions] = None
+ matched_rule_ids: Optional[List[str]] = None
+ application_id: Optional[str] = None
+ organization_id: Optional[str] = None
+ created_at: Optional[int] = None
+ updated_at: Optional[int] = None
diff --git a/nylas/models/scheduler.py b/nylas/models/scheduler.py
new file mode 100644
index 00000000..155e2441
--- /dev/null
+++ b/nylas/models/scheduler.py
@@ -0,0 +1,532 @@
+from dataclasses import dataclass, field
+from typing import Dict, List, Literal, Optional
+
+from dataclasses_json import config, dataclass_json
+from typing_extensions import NotRequired, TypedDict
+
+from nylas.models.availability import AvailabilityRules, OpenHours
+from nylas.models.events import Conferencing, _decode_conferencing
+
+BookingType = Literal["booking", "organizer-confirmation"]
+BookingReminderType = Literal["email", "webhook"]
+BookingRecipientType = Literal["host", "guest", "all"]
+EmailLanguage = Literal["en", "es", "fr", "de", "nl", "sv", "ja", "zh"]
+AdditionalFieldType = Literal[
+ "text",
+ "multi_line_text",
+ "email",
+ "phone_number",
+ "dropdown",
+ "date",
+ "checkbox",
+ "radio_button",
+]
+AdditonalFieldOptionsType = Literal[
+ "text", "email", "phone_number", "date", "checkbox", "radio_button"
+]
+
+
+@dataclass_json
+@dataclass
+class BookingConfirmedTemplate:
+ """
+ Class representation of booking confirmed template settings.
+
+ Attributes:
+ title: The title to replace the default 'Booking Confirmed' title.
+ body: The additional body to be appended after the default body.
+ """
+
+ title: Optional[str] = None
+ body: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class EmailTemplate:
+ """
+ Class representation of email template settings.
+
+ Attributes:
+ logo: The URL to a custom logo that appears at the top of the booking email.
+ booking_confirmed: Configurable settings specifically for booking confirmed emails.
+ """
+
+ # logo: Optional[str] = None
+ booking_confirmed: Optional[BookingConfirmedTemplate] = None
+
+
+@dataclass_json
+@dataclass
+class AdditionalField:
+ """
+ Class representation of an additional field.
+
+ Atributes:
+ label: The text label to be displayed in the Scheduler UI.
+ type: The field type. Supported values are text, multi_line_text,
+ email, phone_number, dropdown, date, checkbox, and radio_button
+ required: Whether the field is required to be filled out by the guest when booking an event.
+ pattern: A regular expression pattern that the value of the field must match.
+ order: The order in which the field will be displayed in the Scheduler UI.
+ Fields with lower order values will be displayed first.
+ options: A list of options for the dropdown or radio_button types.
+ This field is required for the dropdown and radio_button types.
+ """
+
+ label: str
+ type: AdditionalFieldType
+ required: bool
+ pattern: Optional[str] = None
+ order: Optional[int] = None
+ options: Optional[AdditonalFieldOptionsType] = None
+
+
+@dataclass_json
+@dataclass
+class SchedulerSettings:
+ """
+ Class representation of scheduler settings.
+
+ Attributes:
+ additional_fields: Definitions for additional fields to be displayed in the Scheduler UI.
+ available_days_in_future: Number of days in the future that Scheduler is available for scheduling events.
+ min_booking_notice: Minimum number of minutes in the future that a user can make a new booking.
+ min_cancellation_notice: Minimum number of minutes before a booking can be cancelled.
+ cancellation_policy: A message about the cancellation policy to display when booking an event.
+ rescheduling_url: The URL used to reschedule bookings.
+ cancellation_url: The URL used to cancel bookings.
+ organizer_confirmation_url: The URL used to confirm or cancel pending bookings.
+ confirmation_redirect_url: The custom URL to redirect to once the booking is confirmed.
+ hide_rescheduling_options: Whether the option to reschedule an event
+ is hidden in booking confirmations and notifications.
+ hide_cancellation_options: Whether the option to cancel an event
+ is hidden in booking confirmations and notifications.
+ hide_additional_guests: Whether to hide the additional guests field on the scheduling page.
+ email_template: Configurable settings for booking emails.
+ """
+
+ additional_fields: Optional[Dict[str, AdditionalField]] = None
+ available_days_in_future: Optional[int] = None
+ min_booking_notice: Optional[int] = None
+ min_cancellation_notice: Optional[int] = None
+ cancellation_policy: Optional[str] = None
+ rescheduling_url: Optional[str] = None
+ cancellation_url: Optional[str] = None
+ organizer_confirmation_url: Optional[str] = None
+ confirmation_redirect_url: Optional[str] = None
+ hide_rescheduling_options: Optional[bool] = None
+ hide_cancellation_options: Optional[bool] = None
+ hide_additional_guests: Optional[bool] = None
+ email_template: Optional[EmailTemplate] = None
+
+
+@dataclass_json
+@dataclass
+class BookingReminder:
+ """
+ Class representation of a booking reminder.
+
+ Attributes:
+ type: The reminder type.
+ minutes_before_event: The number of minutes before the event to send the reminder.
+ recipient: The recipient of the reminder.
+ email_subject: The subject of the email reminder.
+ """
+
+ type: str
+ minutes_before_event: int
+ recipient: Optional[str] = None
+ email_subject: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class EventBooking:
+ """
+ Class representation of an event booking.
+
+ Attributes:
+ title: The title of the event.
+ description: The description of the event.
+ location: The location of the event.
+ timezone: The timezone for displaying times in confirmation email messages and reminders.
+ booking_type: The type of booking.
+ conferencing: Conference details for the event.
+ disable_emails: Whether Nylas sends email messages when an event is booked, cancelled, or rescheduled.
+ reminders: The list of reminders to send to participants before the event starts.
+ """
+
+ title: str
+ description: Optional[str] = None
+ location: Optional[str] = None
+ timezone: Optional[str] = None
+ booking_type: Optional[BookingType] = None
+ conferencing: Optional[Conferencing] = field(
+ default=None, metadata=config(decoder=_decode_conferencing)
+ )
+ disable_emails: Optional[bool] = None
+ reminders: Optional[List[BookingReminder]] = None
+
+
+@dataclass_json
+@dataclass
+class Availability:
+ """
+ Class representation of availability settings.
+
+ Attributes:
+ duration_minutes: The total number of minutes the event should last.
+ interval_minutes: The interval between meetings in minutes.
+ round_to: Nylas rounds each time slot to the nearest multiple of this number of minutes.
+ availability_rules: Availability rules for scheduling configuration.
+ """
+
+ duration_minutes: int
+ interval_minutes: Optional[int] = None
+ round_to: Optional[int] = None
+ availability_rules: Optional[AvailabilityRules] = None
+
+
+@dataclass_json
+@dataclass
+class ParticipantBooking:
+ """
+ Class representation of a participant booking.
+
+ Attributes:
+ calendar_id: The calendar ID that the event is created in.
+ """
+
+ calendar_id: str
+
+
+@dataclass_json
+@dataclass
+class ParticipantAvailability:
+ """
+ Class representation of participant availability.
+
+ Attributes:
+ calendar_ids: List of calendar IDs associated with the participant's email address.
+ open_hours: Open hours for this participant. The endpoint searches for free time slots during these open hours.
+ """
+
+ calendar_ids: List[str]
+ open_hours: Optional[List[OpenHours]] = None
+
+
+@dataclass_json
+@dataclass
+class ConfigParticipant:
+ """
+ Class representation of a booking participant.
+
+ Attributes:
+ email: Participant's email address.
+ availability: Availability data for the participant.
+ booking: Booking data for the participant.
+ name: Participant's name.
+ is_organizer: Whether the participant is the organizer of the event.
+ timezone: The participant's timezone.
+ """
+
+ email: str
+ availability: ParticipantAvailability
+ booking: ParticipantBooking
+ name: Optional[str] = None
+ is_organizer: Optional[bool] = None
+ timezone: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class Configuration:
+ """
+ Class representation of a scheduler configuration.
+
+ Attributes:
+ participants: List of participants included in the scheduled event.
+ availability: Rules that determine available time slots for the event.
+ event_booking: Booking data for the event.
+ slug: Unique identifier for the Configuration object.
+ requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID.
+ scheduler: Settings for the Scheduler UI.
+ appearance: Appearance settings for the Scheduler UI.
+ """
+
+ id: str
+ participants: List[ConfigParticipant]
+ availability: Availability
+ event_booking: EventBooking
+ slug: Optional[str] = None
+ requires_session_auth: Optional[bool] = None
+ scheduler: Optional[SchedulerSettings] = None
+ appearance: Optional[Dict[str, str]] = None
+
+
+class CreateConfigurationRequest(TypedDict):
+ """
+ Interface of a Nylas create configuration request.
+
+ Attributes:
+ participants: List of participants included in the scheduled event.
+ availability: Rules that determine available time slots for the event.
+ event_booking: Booking data for the event.
+ slug: Unique identifier for the Configuration object.
+ requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID.
+ scheduler: Settings for the Scheduler UI.
+ appearance: Appearance settings for the Scheduler UI.
+ """
+
+ participants: List[ConfigParticipant]
+ availability: Availability
+ event_booking: EventBooking
+ slug: NotRequired[str]
+ requires_session_auth: NotRequired[bool]
+ scheduler: NotRequired[SchedulerSettings]
+ appearance: NotRequired[Dict[str, str]]
+
+
+class UpdateConfigurationRequest(TypedDict):
+ """
+ Interface of a Nylas update configuration request.
+
+ Attributes:
+ participants: List of participants included in the scheduled event.
+ availability: Rules that determine available time slots for the event.
+ event_booking: Booking data for the event.
+ slug: Unique identifier for the Configuration object.
+ requires_session_auth: If true, scheduling Availability and Bookings endpoints require a valid session ID.
+ scheduler: Settings for the Scheduler UI.
+ appearance: Appearance settings for the Scheduler UI.
+ """
+
+ participants: NotRequired[List[ConfigParticipant]]
+ availability: NotRequired[Availability]
+ event_booking: NotRequired[EventBooking]
+ slug: NotRequired[str]
+ requires_session_auth: NotRequired[bool]
+ scheduler: NotRequired[SchedulerSettings]
+ appearance: NotRequired[Dict[str, str]]
+
+
+class CreateSessionRequest(TypedDict):
+ """
+ Interface of a Nylas create session request.
+
+ Attributes:
+ configuration_id: The ID of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true),
+ configuration_id is not required.
+ slug: The slug of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true) or using configurationId,
+ slug is not required.
+ time_to_live: The time-to-live in seconds for the session
+ """
+
+ configuration_id: NotRequired[str]
+ slug: NotRequired[str]
+ time_to_live: NotRequired[int]
+
+
+@dataclass_json
+@dataclass
+class Session:
+ """
+ Class representation of a session.
+
+ Attributes:
+ session_id: The ID of the session.
+ """
+
+ session_id: str
+
+
+@dataclass_json
+@dataclass
+class BookingGuest:
+ """
+ Class representation of a booking guest.
+
+ Attributes:
+ email: The email address of the guest.
+ name: The name of the guest.
+ """
+
+ email: str
+ name: str
+
+
+@dataclass_json
+@dataclass
+class BookingParticipant:
+ """
+ Class representation of a booking participant.
+
+ Attributes:
+ email: The email address of the participant to include in the booking.
+ """
+
+ email: str
+
+
+@dataclass_json
+@dataclass
+class CreateBookingRequest:
+ """
+ Class representation of a create booking request.
+
+ Attributes:
+ start_time: The event's start time, in Unix epoch format.
+ end_time: The event's end time, in Unix epoch format.
+ guest: Details about the guest that is creating the booking.
+ participants: List of participant email addresses from the
+ Configuration object to include in the booking.
+ timezone: The guest's timezone that is used in email notifications.
+ email_language: The language of the guest email notifications.
+ additional_guests: List of additional guest email addresses to include in the booking.
+ additional_fields: Dictionary of additional field keys mapped to
+ values populated by the guest in the booking form.
+ """
+
+ start_time: int
+ end_time: int
+ guest: BookingGuest
+ participants: Optional[List[BookingParticipant]] = None
+ timezone: Optional[str] = None
+ email_language: Optional[EmailLanguage] = None
+ additional_guests: Optional[List[BookingGuest]] = None
+ additional_fields: Optional[Dict[str, str]] = None
+
+
+@dataclass_json
+@dataclass
+class BookingOrganizer:
+ """
+ Class representation of a booking organizer.
+
+ Attributes:
+ email: The email address of the participant designated as the organizer of the event.
+ name: The name of the participant designated as the organizer of the event.
+ """
+
+ email: str
+ name: Optional[str] = None
+
+
+BookingStatus = Literal["pending", "confirmed", "cancelled"]
+ConfirmBookingStatus = Literal["confirm", "cancel"]
+
+
+@dataclass_json
+@dataclass
+class Booking:
+ """
+ Class representation of a booking.
+
+ Attributes:
+ booking_id: The unique ID of the booking.
+ event_id: The unique ID of the event associated with the booking.
+ title: The title of the event.
+ organizer: The participant designated as the organizer of the event.
+ status: The current status of the booking.
+ description: The description of the event.
+ """
+
+ booking_id: str
+ event_id: str
+ title: str
+ organizer: BookingOrganizer
+ status: BookingStatus
+ description: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class ConfirmBookingRequest:
+ """
+ Class representation of a confirm booking request.
+
+ Attributes:
+ salt: The salt extracted from the booking reference embedded in the organizer confirmation link.
+ status: The action to take on the pending booking.
+ cancellation_reason: The reason the booking is being cancelled.
+ """
+
+ salt: str
+ status: ConfirmBookingStatus
+ cancellation_reason: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class DeleteBookingRequest:
+ """
+ Class representation of a delete booking request.
+
+ Attributes:
+ cancellation_reason: The reason the booking is being cancelled.
+ """
+
+ cancellation_reason: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class RescheduleBookingRequest:
+ """
+ Class representation of a reschedule booking request.
+
+ Attributes:
+ start_time: The event's start time, in Unix epoch format.
+ end_time: The event's end time, in Unix epoch format.
+ """
+
+ start_time: int
+ end_time: int
+
+
+@dataclass_json
+@dataclass
+class CreateBookingQueryParams:
+ """
+ Class representation of query parameters for creating a booking.
+
+ Attributes:
+ configuration_id: The ID of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true), configuration_id is not required.
+ slug: The slug of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true) or using configurationId,
+ slug is not required.
+ timezone: The timezone to use for the booking.
+ If not provided, Nylas uses the timezone from the Configuration object.
+ """
+
+ configuration_id: Optional[str] = None
+ slug: Optional[str] = None
+ timezone: Optional[str] = None
+
+
+class FindBookingQueryParams:
+ """
+ Class representation of query parameters for finding a booking.
+
+ Attributes:
+ configuration_id: The ID of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true), configuration_id is not required.
+ slug: The slug of the Configuration object whose settings are used for calculating availability.
+ If you're using session authentication (requires_session_auth is set to true)
+ or using configurationId, slug is not required.
+ client_id: The client ID that was used to create the Configuration object.
+ client_id is required only if using slug.
+ """
+
+ configuration_id: Optional[str] = None
+ slug: Optional[str] = None
+ client_id: Optional[str] = None
+
+
+ConfirmBookingQueryParams = FindBookingQueryParams
+RescheduleBookingQueryParams = FindBookingQueryParams
+DestroyBookingQueryParams = FindBookingQueryParams
diff --git a/nylas/models/smart_compose.py b/nylas/models/smart_compose.py
new file mode 100644
index 00000000..05fa2a3a
--- /dev/null
+++ b/nylas/models/smart_compose.py
@@ -0,0 +1,28 @@
+from dataclasses import dataclass
+from typing import TypedDict
+
+from dataclasses_json import dataclass_json
+
+
+class ComposeMessageRequest(TypedDict):
+ """
+ A request to compose a message.
+
+ Attributes:
+ prompt: The prompt that smart compose will use to generate a message suggestion.
+ """
+
+ prompt: str
+
+
+@dataclass_json
+@dataclass
+class ComposeMessageResponse:
+ """
+ A response from composing a message.
+
+ Attributes:
+ suggestion: The message suggestion generated by smart compose.
+ """
+
+ suggestion: str
diff --git a/nylas/models/threads.py b/nylas/models/threads.py
new file mode 100644
index 00000000..17798e87
--- /dev/null
+++ b/nylas/models/threads.py
@@ -0,0 +1,148 @@
+from dataclasses import dataclass, field
+from typing import List, Optional, get_type_hints, Union
+from typing_extensions import TypedDict, NotRequired
+
+from dataclasses_json import dataclass_json, config
+
+from nylas.models.drafts import Draft
+from nylas.models.events import EmailName
+from nylas.models.list_query_params import ListQueryParams
+
+from nylas.models.messages import Message
+
+
+def _decode_draft_or_message(json: dict) -> Union[Message, Draft]:
+ """
+ Decode a message/draft object into a python object.
+
+ Args:
+ json: The message/draft object to decode.
+
+ Returns:
+ The decoded message/draft object.
+ """
+ if "object" not in json:
+ raise ValueError("Invalid when object, no 'object' field found.")
+
+ if json["object"] == "draft":
+ return Draft.from_dict(json)
+
+ if json["object"] == "message":
+ return Message.from_dict(json)
+
+ raise ValueError(f"Invalid object, unknown 'object' field found: {json['object']}")
+
+
+@dataclass_json
+@dataclass
+class Thread:
+ """
+ A Thread object.
+
+ Attributes:
+ id: Globally unique object identifier.
+ grant_id: The grant that this thread belongs to.
+ latest_draft_or_message: The latest draft or message in the thread.
+ has_attachment: Whether the thread has an attachment.
+ has_drafts: Whether the thread has drafts.
+ starred: A boolean indicating whether the thread is starred or not
+ unread: A boolean indicating whether the thread is read or not.
+ earliest_message_date: Unix timestamp of the earliest or first message in the thread.
+ latest_message_received_date: Unix timestamp of the most recent message received in the thread.
+ latest_message_sent_date: Unix timestamp of the most recent message sent in the thread.
+ participant: An array of participants in the thread.
+ message_ids: An array of message IDs in the thread.
+ draft_ids: An array of draft IDs in the thread.
+ folders: An array of folder IDs the thread appears in.
+ object: The type of object.
+ snippet: A short snippet of the last received message/draft body.
+ This is the first 100 characters of the message body, with any HTML tags removed.
+ subject: The subject of the thread.
+ """
+
+ id: str
+ grant_id: str
+ has_drafts: bool
+ starred: bool
+ unread: bool
+ message_ids: List[str]
+ folders: List[str]
+ latest_draft_or_message: Union[Message, Draft] = field(
+ metadata=config(decoder=_decode_draft_or_message)
+ )
+ object: str = "thread"
+ earliest_message_date: Optional[int] = None
+ latest_message_received_date: Optional[int] = None
+ draft_ids: Optional[List[str]] = None
+ snippet: Optional[str] = None
+ subject: Optional[str] = None
+ participants: Optional[List[EmailName]] = None
+ latest_message_sent_date: Optional[int] = None
+ has_attachments: Optional[bool] = None
+
+
+class UpdateThreadRequest(TypedDict):
+ """
+ A request to update a thread.
+
+ Attributes:
+ starred: Sets all messages in the thread as starred or unstarred.
+ unread: Sets all messages in the thread as read or unread.
+ folders: The IDs of the folders to apply, overwriting all previous folders for all messages in the thread.
+ """
+
+ starred: NotRequired[bool]
+ unread: NotRequired[bool]
+ folders: NotRequired[List[str]]
+
+
+# Need to use Functional typed dicts because "from" and "in" are Python
+# keywords, and can't be declared using the declarative syntax
+ListThreadsQueryParams = TypedDict(
+ "ListThreadsQueryParams",
+ {
+ **get_type_hints(ListQueryParams), # Inherit fields from ListQueryParams
+ "subject": NotRequired[str],
+ "any_email": NotRequired[str],
+ "from": NotRequired[str],
+ "to": NotRequired[str],
+ "cc": NotRequired[str],
+ "bcc": NotRequired[str],
+ "in": NotRequired[str],
+ "unread": NotRequired[bool],
+ "starred": NotRequired[bool],
+ "thread_id": NotRequired[str],
+ "earliest_message_date": NotRequired[int],
+ "latest_message_before": NotRequired[int],
+ "latest_message_after": NotRequired[int],
+ "has_attachment": NotRequired[bool],
+ "search_query_native": NotRequired[str],
+ "select": NotRequired[str],
+ },
+)
+"""
+Query parameters for listing threads.
+
+Attributes:
+ subject: Return threads with matching subject.
+ any_email: Return threads that have been sent or received by this comma-separated list of email addresses.
+ from: Return threads sent from this email address.
+ to: Return threads sent to this email address.
+ cc: Return threads cc'd to this email address.
+ bcc: Return threads bcc'd to this email address.
+ in: Return threads in this specific folder or label, specified by ID.
+ unread: Filter threads by unread status.
+ starred: Filter threads by starred status.
+ thread_id: Filter threads by thread_id.
+ earliest_message_date: Unix timestamp of the earliest or first message in the thread.
+ latest_message_before: Return threads whose most recent message was received before this Unix timestamp.
+ latest_message_after: Return threads whose most recent message was received after this Unix timestamp.
+ has_attachment: Filter threads by whether they have an attachment.
+ search_query_native: A native provider search query for Google or Microsoft.
+ select: Comma-separated list of fields to return in the response.
+ This allows you to receive only the portion of object data that you're interested in.
+ limit (NotRequired[int]): The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token (NotRequired[str]): An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+"""
diff --git a/nylas/models/transactional_send.py b/nylas/models/transactional_send.py
new file mode 100644
index 00000000..b9b4a815
--- /dev/null
+++ b/nylas/models/transactional_send.py
@@ -0,0 +1,63 @@
+from typing import Any, Dict, List
+
+from typing_extensions import NotRequired, Required, TypedDict
+
+from nylas.models.attachments import CreateAttachmentRequest
+from nylas.models.drafts import CustomHeader, TrackingOptions
+from nylas.models.events import EmailName
+
+
+class TransactionalTemplate(TypedDict, total=False):
+ """
+ Template selection for a transactional send request.
+
+ Attributes:
+ id: The template ID.
+ strict: When true, Nylas returns an error if the template contains undefined variables.
+ variables: Key/value pairs substituted into the template.
+ """
+
+ id: Required[str]
+ strict: NotRequired[bool]
+ variables: NotRequired[Dict[str, Any]]
+
+
+class TransactionalSendMessageRequest(TypedDict, total=False):
+ """
+ Request body for POST /v3/domains/{domain_name}/messages/send.
+
+ Use ``from_`` for the sender; it is serialized as JSON ``from`` (``from`` is a Python keyword).
+
+ Attributes:
+ to: Recipients (required by the API).
+ from_: Sender ``email`` / optional ``name`` (required by the API).
+ subject: Subject line.
+ body: HTML or plain body depending on ``is_plaintext``.
+ cc: CC recipients.
+ bcc: BCC recipients.
+ reply_to: Reply-To recipients.
+ attachments: File attachments.
+ send_at: Unix timestamp to send the message later.
+ reply_to_message_id: Message being replied to.
+ tracking_options: Open/link tracking settings.
+ custom_headers: Custom MIME headers.
+ metadata: String-keyed metadata.
+ is_plaintext: Send body as plain text when true.
+ template: Application template to render (optional vs. body/subject).
+ """
+
+ to: Required[List[EmailName]]
+ from_: Required[EmailName]
+ subject: NotRequired[str]
+ body: NotRequired[str]
+ cc: NotRequired[List[EmailName]]
+ bcc: NotRequired[List[EmailName]]
+ reply_to: NotRequired[List[EmailName]]
+ attachments: NotRequired[List[CreateAttachmentRequest]]
+ send_at: NotRequired[int]
+ reply_to_message_id: NotRequired[str]
+ tracking_options: NotRequired[TrackingOptions]
+ custom_headers: NotRequired[List[CustomHeader]]
+ metadata: NotRequired[Dict[str, Any]]
+ is_plaintext: NotRequired[bool]
+ template: NotRequired[TransactionalTemplate]
diff --git a/nylas/models/webhooks.py b/nylas/models/webhooks.py
new file mode 100644
index 00000000..7cbee4a1
--- /dev/null
+++ b/nylas/models/webhooks.py
@@ -0,0 +1,167 @@
+from dataclasses import dataclass
+from enum import Enum
+from typing import List, Optional, Literal
+
+from dataclasses_json import dataclass_json
+from typing_extensions import TypedDict, NotRequired
+
+WebhookStatus = Literal["active", "failing", "failed", "pause"]
+""" Literals representing the possible webhook statuses. """
+
+
+class WebhookTriggers(str, Enum):
+ """Enum representing the available webhook triggers."""
+ BOOKING_CREATED = "booking.created"
+ BOOKING_PENDING = "booking.pending"
+ BOOKING_RESCHEDULED = "booking.rescheduled"
+ BOOKING_CANCELLED = "booking.cancelled"
+ BOOKING_REMINDER = "booking.reminder"
+ CALENDAR_CREATED = "calendar.created"
+ CALENDAR_UPDATED = "calendar.updated"
+ CALENDAR_DELETED = "calendar.deleted"
+ CONTACT_UPDATED = "contact.updated"
+ CONTACT_DELETED = "contact.deleted"
+ EVENT_CREATED = "event.created"
+ EVENT_UPDATED = "event.updated"
+ EVENT_DELETED = "event.deleted"
+ GRANT_CREATED = "grant.created"
+ GRANT_UPDATED = "grant.updated"
+ GRANT_DELETED = "grant.deleted"
+ GRANT_EXPIRED = "grant.expired"
+ MESSAGE_SEND_SUCCESS = "message.send_success"
+ MESSAGE_SEND_FAILED = "message.send_failed"
+ MESSAGE_BOUNCE_DETECTED = "message.bounce_detected"
+ MESSAGE_CREATED = "message.created"
+ MESSAGE_UPDATED = "message.updated"
+ MESSAGE_DELETED= "message.deleted"
+ MESSAGE_OPENED = "message.opened"
+ MESSAGE_LINK_CLICKED = "message.link_clicked"
+ MESSAGE_OPENED_LEGACY = "message.opened.legacy"
+ MESSAGE_LINK_CLICKED_LEGACY = "message.link_clicked.legacy"
+ MESSAGE_INTELLIGENCE_ORDER = "message.intelligence.order"
+ MESSAGE_INTELLIGENCE_TRACKING = "message.intelligence.tracking"
+ MESSAGE_INTELLIGENCE_RETURN = "message.intelligence.return"
+ THREAD_REPLIED = "thread.replied"
+ THREAD_REPLIED_LEGACY = "thread.replied.legacy"
+ FOLDER_CREATED = "folder.created"
+ FOLDER_UPDATED = "folder.updated"
+ FOLDER_DELETED = "folder.deleted"
+
+
+@dataclass_json
+@dataclass
+class Webhook:
+ """
+ Class representing a Nylas webhook.
+
+ Attributes:
+ id: Globally unique object identifier.
+ trigger_types: The event that triggers the webhook.
+ webhook_url: The URL to send webhooks to.
+ status: The status of the new destination.
+ notification_email_addresses: The email addresses that Nylas notifies when a webhook is down for a while.
+ status_updated_at: The time when the status field was last updated, represented as a Unix timestamp in seconds.
+ created_at: The time when the status field was created, represented as a Unix timestamp in seconds.
+ updated_at: The time when the status field was last updated, represented as a Unix timestamp in seconds.
+ description: A human-readable description of the webhook destination.
+ """
+
+ id: str
+ trigger_types: List[WebhookTriggers]
+ webhook_url: str
+ status: WebhookStatus
+ notification_email_addresses: List[str]
+ status_updated_at: int
+ created_at: int
+ updated_at: int
+ description: Optional[str] = None
+
+
+@dataclass_json
+@dataclass
+class WebhookWithSecret(Webhook):
+ """
+ Class representing a Nylas webhook with secret.
+
+ Attributes:
+ webhook_secret: A secret value used to encode the X-Nylas-Signature header on webhook requests.
+ """
+
+ webhook_secret: str = ""
+
+
+@dataclass_json
+@dataclass
+class WebhookDeleteData:
+ """
+ Class representing the object enclosing the webhook deletion status.
+
+ Attributes:
+ status: The status of the webhook deletion.
+ """
+
+ status: str
+
+
+@dataclass_json
+@dataclass
+class WebhookDeleteResponse:
+ """
+ Class representing a Nylas webhook delete response.
+
+ Attributes:
+ request_id: The request's ID.
+ data: Object containing the webhook deletion status.
+ """
+
+ request_id: str
+ data: Optional[WebhookDeleteData] = None
+
+
+@dataclass_json
+@dataclass
+class WebhookIpAddressesResponse:
+ """
+ Class representing the response for getting a list of webhook IP addresses.
+
+ Attributes:
+ ip_addresses: The IP addresses that Nylas send your webhook from.
+ updated_at: Unix timestamp representing the time when Nylas last updated the list of IP addresses.
+ """
+
+ ip_addresses: List[str]
+ updated_at: int
+
+
+class CreateWebhookRequest(TypedDict):
+ """
+ Class representation of a Nylas create webhook request.
+
+ Attributes:
+ trigger_types: List of events that triggers the webhook.
+ webhook_url: The url to send webhooks to.
+ description: A human-readable description of the webhook destination.
+ notification_email_addresses: The email addresses that Nylas notifies when a webhook is down for a while.
+ """
+
+ trigger_types: List[WebhookTriggers]
+ webhook_url: str
+ description: NotRequired[str]
+ notification_email_addresses: NotRequired[List[str]]
+
+
+class UpdateWebhookRequest(TypedDict):
+ """
+ Class representation of a Nylas update webhook request.
+
+ Attributes:
+ trigger_types: List of events that triggers the webhook.
+ webhook_url: The url to send webhooks to.
+ description: A human-readable description of the webhook destination.
+ notification_email_addresses: The email addresses that Nylas notifies when a webhook is down for a while.
+ """
+
+ trigger_types: NotRequired[List[WebhookTriggers]]
+ webhook_url: NotRequired[str]
+ description: NotRequired[str]
+ notification_email_addresses: NotRequired[List[str]]
diff --git a/nylas/resources/__init__.py b/nylas/resources/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nylas/resources/applications.py b/nylas/resources/applications.py
new file mode 100644
index 00000000..ed20c5ae
--- /dev/null
+++ b/nylas/resources/applications.py
@@ -0,0 +1,40 @@
+from nylas.config import RequestOverrides
+from nylas.models.application_details import ApplicationDetails
+from nylas.models.response import Response
+from nylas.resources.redirect_uris import RedirectUris
+from nylas.resources.resource import Resource
+
+
+class Applications(Resource):
+ """
+ Nylas Applications API
+
+ The Nylas Applications API allows you to get information about your Nylas application.
+ You can also manage the redirect URIs associated with your application.
+ """
+
+ @property
+ def redirect_uris(self) -> RedirectUris:
+ """
+ Manage Redirect URIs for your Nylas Application.
+
+ Returns:
+ RedirectUris: The redirect URIs associated with your Nylas Application.
+ """
+ return RedirectUris(self._http_client)
+
+ def info(self, overrides: RequestOverrides = None) -> Response[ApplicationDetails]:
+ """
+ Get the application information.
+
+ Args:
+ overrides: The query parameters to include in the request.
+
+ Returns:
+ Response: The application information.
+ """
+
+ json_response, headers = self._http_client._execute(
+ method="GET", path="/v3/applications", overrides=overrides
+ )
+ return Response.from_dict(json_response, ApplicationDetails, headers)
diff --git a/nylas/resources/attachments.py b/nylas/resources/attachments.py
new file mode 100644
index 00000000..1c4616b5
--- /dev/null
+++ b/nylas/resources/attachments.py
@@ -0,0 +1,177 @@
+from requests import Response
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ FindableApiResource,
+ CreatableApiResource,
+)
+from nylas.models.attachments import (
+ Attachment,
+ FindAttachmentQueryParams,
+ CreateAttachmentUploadSessionRequest,
+ AttachmentUploadSession,
+ AttachmentUploadSessionComplete,
+)
+from nylas.models.response import Response as NylasResponse
+
+
+class Attachments(
+ FindableApiResource,
+ CreatableApiResource,
+):
+ """
+ Nylas Attachments API
+
+ The Nylas Attachments API allows you to get metadata ot, and download attachments from messages.
+ """
+
+ def find(
+ self,
+ identifier: str,
+ attachment_id: str,
+ query_params: FindAttachmentQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> NylasResponse[Attachment]:
+ """
+ Return metadata of an attachment.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ attachment_id: The id of the attachment to retrieve.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The attachment metadata.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/attachments/{attachment_id}",
+ response_type=Attachment,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def download(
+ self,
+ identifier: str,
+ attachment_id: str,
+ query_params: FindAttachmentQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> Response:
+ """
+ Download the attachment data.
+
+ This function returns a raw response object to allow you the ability
+ to stream the file contents. The response object should be closed
+ after use to ensure the connection is closed.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ attachment_id: The id of the attachment to download.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Response object containing the file data.
+
+ Example:
+ Here is an example of how to use this function when streaming:
+
+ ```python
+ response = execute_request_raw_response(url, method, stream=True)
+ try:
+ for chunk in response.iter_content(chunk_size=8192):
+ if chunk:
+ # Process each chunk
+ pass
+ finally:
+ response.close() # Ensure the response is closed
+ ```
+ """
+ return self._http_client._execute_download_request(
+ path=f"/v3/grants/{identifier}/attachments/{attachment_id}/download",
+ query_params=query_params,
+ stream=True,
+ overrides=overrides,
+ )
+
+ def download_bytes(
+ self,
+ identifier: str,
+ attachment_id: str,
+ query_params: FindAttachmentQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> bytes:
+ """
+ Download the attachment as a byte array.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ attachment_id: The id of the attachment to download.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The raw file data.
+ """
+ return self._http_client._execute_download_request(
+ path=f"/v3/grants/{identifier}/attachments/{attachment_id}/download",
+ query_params=query_params,
+ stream=False,
+ overrides=overrides,
+ )
+
+ def create_upload_session(
+ self,
+ identifier: str,
+ request_body: CreateAttachmentUploadSessionRequest,
+ overrides: RequestOverrides = None,
+ ) -> NylasResponse[AttachmentUploadSession]:
+ """
+ Create a resumable upload session for a large attachment (up to 150 MB).
+
+ After receiving the session, upload file bytes via HTTP PUT to the returned
+ `url` (include the returned `headers`; no Nylas auth header needed), then
+ call complete_upload_session() with the returned `attachment_id`.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ request_body: Session parameters (filename, content_type, optional size).
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The upload session, including the pre-signed URL and attachment_id.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/attachment-uploads",
+ response_type=AttachmentUploadSession,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def complete_upload_session(
+ self,
+ identifier: str,
+ attachment_id: str,
+ overrides: RequestOverrides = None,
+ ) -> NylasResponse[AttachmentUploadSessionComplete]:
+ """
+ Complete an upload session after file bytes have been PUT to the pre-signed URL.
+
+ Use the `attachment_id` from the completed session when referencing the
+ attachment in a subsequent messages.send() or drafts.create() call.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ attachment_id: The upload session ID returned by create_upload_session().
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The completed session status.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/attachment-uploads/{attachment_id}/complete",
+ response_type=AttachmentUploadSessionComplete,
+ request_body={},
+ overrides=overrides,
+ )
diff --git a/nylas/resources/auth.py b/nylas/resources/auth.py
new file mode 100644
index 00000000..6129bb28
--- /dev/null
+++ b/nylas/resources/auth.py
@@ -0,0 +1,286 @@
+import base64
+import hashlib
+import uuid
+
+from nylas.config import RequestOverrides
+from nylas.handler.http_client import _build_query_params
+from nylas.models.grants import CreateGrantRequest, Grant
+
+from nylas.models.auth import (
+ CodeExchangeResponse,
+ PkceAuthUrl,
+ TokenInfoResponse,
+ CodeExchangeRequest,
+ TokenExchangeRequest,
+ ProviderDetectResponse,
+ ProviderDetectParams,
+ URLForAuthenticationConfig,
+ URLForAdminConsentConfig,
+)
+from nylas.models.response import Response
+from nylas.resources.resource import Resource
+
+
+def _hash_pkce_secret(secret: str) -> str:
+ sha256_hash = hashlib.sha256(secret.encode()).hexdigest()
+ return base64.b64encode(sha256_hash.encode()).decode().rstrip("=")
+
+
+def _build_query(config: dict) -> dict:
+ config["response_type"] = "code"
+
+ if "access_type" not in config:
+ config["access_type"] = "online"
+
+ if "scope" in config:
+ config["scope"] = " ".join(config["scope"])
+
+ if config.pop("smtp_required", None):
+ config["options"] = "smtp_required"
+
+ return config
+
+
+def _build_query_with_pkce(config: dict, secret_hash: str) -> dict:
+ params = _build_query(config)
+
+ params["code_challenge"] = secret_hash
+ params["code_challenge_method"] = "s256"
+
+ return params
+
+
+def _build_query_with_admin_consent(config: dict) -> dict:
+ params = _build_query(config)
+
+ params["response_type"] = "adminconsent"
+
+ if "credential_id" in config:
+ params["credential_id"] = config["credential_id"]
+
+ return params
+
+
+class Auth(Resource):
+ """
+ A collection of authentication related API endpoints
+
+ These endpoints allow for various functionality related to authentication.
+ """
+
+ def url_for_oauth2(self, config: URLForAuthenticationConfig) -> str:
+ """
+ Build the URL for authenticating users to your application via Hosted Authentication.
+
+ Args:
+ config: The configuration for building the URL.
+
+ Returns:
+ The URL for hosted authentication.
+ """
+ query = _build_query(config)
+
+ return self._url_auth_builder(query)
+
+ def exchange_code_for_token(
+ self, request: CodeExchangeRequest, overrides: RequestOverrides = None
+ ) -> CodeExchangeResponse:
+ """
+ Exchange an authorization code for an access token.
+
+ Args:
+ request: The request parameters for the code exchange
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ Information about the Nylas application
+ """
+ if "client_secret" not in request:
+ request["client_secret"] = self._http_client.api_key
+
+ request_body = dict(request)
+ request_body["grant_type"] = "authorization_code"
+
+ return self._get_token(request_body, overrides)
+
+ def custom_authentication(
+ self, request_body: CreateGrantRequest, overrides: RequestOverrides = None
+ ) -> Response[Grant]:
+ """
+ Create a Grant via Custom Authentication.
+
+ Args:
+ request_body: The values to create the Grant with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created Grant.
+ """
+
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path="/v3/connect/custom",
+ request_body=request_body,
+ overrides=overrides,
+ )
+ return Response.from_dict(json_response, Grant, headers)
+
+ def refresh_access_token(
+ self, request: TokenExchangeRequest, overrides: RequestOverrides = None
+ ) -> CodeExchangeResponse:
+ """
+ Refresh an access token.
+
+ Args:
+ request: The refresh token request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The response containing the new access token.
+ """
+ if "client_secret" not in request:
+ request["client_secret"] = self._http_client.api_key
+
+ request_body = dict(request)
+ request_body["grant_type"] = "refresh_token"
+
+ return self._get_token(request_body, overrides)
+
+ def id_token_info(
+ self, id_token: str, overrides: RequestOverrides = None
+ ) -> Response[TokenInfoResponse]:
+ """
+ Get info about an ID token.
+
+ Args:
+ id_token: The ID token to query.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The API response with the token information.
+ """
+
+ query_params = {
+ "id_token": id_token,
+ }
+
+ return self._get_token_info(query_params, overrides)
+
+ def validate_access_token(
+ self, access_token: str, overrides: RequestOverrides = None
+ ) -> Response[TokenInfoResponse]:
+ """
+ Get info about an access token.
+
+ Args:
+ access_token: The access token to query.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The API response with the token information.
+ """
+
+ query_params = {
+ "access_token": access_token,
+ }
+
+ return self._get_token_info(query_params, overrides)
+
+ def url_for_oauth2_pkce(self, config: URLForAuthenticationConfig) -> PkceAuthUrl:
+ """
+ Build the URL for authenticating users to your application via Hosted Authentication with PKCE.
+
+ IMPORTANT: YOU WILL NEED TO STORE THE 'secret' returned to use it inside the CodeExchange flow
+
+ Args:
+ config: The configuration for the authentication request.
+
+ Returns:
+ The URL for hosted authentication with secret & hashed secret.
+ """
+ secret = str(uuid.uuid4())
+ secret_hash = _hash_pkce_secret(secret)
+ query = _build_query_with_pkce(config, secret_hash)
+
+ return PkceAuthUrl(secret, secret_hash, self._url_auth_builder(query))
+
+ def url_for_admin_consent(self, config: URLForAdminConsentConfig) -> str:
+ """Build the URL for admin consent authentication for Microsoft.
+
+ Args:
+ config: The configuration for the authentication request.
+
+ Returns:
+ The URL for hosted authentication.
+ """
+ config_with_provider = {"provider": "microsoft", **config}
+ query = _build_query_with_admin_consent(config_with_provider)
+
+ return self._url_auth_builder(query)
+
+ def revoke(self, token: str, overrides: RequestOverrides = None) -> True:
+ """Revoke a single access token.
+
+ Args:
+ token: The access token to revoke.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ True: If the token was revoked successfully.
+ """
+ self._http_client._execute(
+ method="POST",
+ path="/v3/connect/revoke",
+ query_params={"token": token},
+ overrides=overrides,
+ )
+
+ return True
+
+ def detect_provider(
+ self, params: ProviderDetectParams, overrides: RequestOverrides = None
+ ) -> Response[ProviderDetectResponse]:
+ """
+ Detect provider from email address.
+
+ Args:
+ params: The parameters to include in the request
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The detected provider, if found.
+ """
+
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path="/v3/providers/detect",
+ query_params=params,
+ overrides=overrides,
+ )
+ return Response.from_dict(json_response, ProviderDetectResponse, headers)
+
+ def _url_auth_builder(self, query: dict) -> str:
+ base = f"{self._http_client.api_server}/v3/connect/auth"
+ return _build_query_params(base, query)
+
+ def _get_token(
+ self, request_body: dict, overrides: RequestOverrides
+ ) -> CodeExchangeResponse:
+ json_response, _ = self._http_client._execute(
+ method="POST",
+ path="/v3/connect/token",
+ request_body=request_body,
+ overrides=overrides,
+ )
+ return CodeExchangeResponse.from_dict(json_response)
+
+ def _get_token_info(
+ self, query_params: dict, overrides: RequestOverrides
+ ) -> Response[TokenInfoResponse]:
+ json_response, headers = self._http_client._execute(
+ method="GET",
+ path="/v3/connect/tokeninfo",
+ query_params=query_params,
+ overrides=overrides,
+ )
+ return Response.from_dict(json_response, TokenInfoResponse, headers)
diff --git a/nylas/resources/bookings.py b/nylas/resources/bookings.py
new file mode 100644
index 00000000..0fa3cf55
--- /dev/null
+++ b/nylas/resources/bookings.py
@@ -0,0 +1,176 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+ UpdatablePatchApiResource,
+)
+from nylas.models.response import DeleteResponse, Response
+from nylas.models.scheduler import (
+ Booking,
+ ConfirmBookingQueryParams,
+ ConfirmBookingRequest,
+ CreateBookingQueryParams,
+ CreateBookingRequest,
+ DeleteBookingRequest,
+ DestroyBookingQueryParams,
+ RescheduleBookingRequest,
+ FindBookingQueryParams,
+ RescheduleBookingQueryParams,
+)
+
+
+class Bookings(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ UpdatablePatchApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Bookings API
+
+ The Nylas Bookings API allows you to create new bookings or manage existing ones, as well as getting
+ bookings details for a user.
+
+ A booking can be accessed by one, or several people, and can contain events.
+ """
+
+ def find(
+ self,
+ booking_id: str,
+ query_params: FindBookingQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Booking]:
+ """
+ Return a Booking.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ booking_id: The identifier of the Booking to get.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Booking.
+ """
+
+ return super().find(
+ path=f"/v3/scheduling/bookings/{booking_id}",
+ query_params=query_params,
+ response_type=Booking,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ request_body: CreateBookingRequest,
+ query_params: CreateBookingQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Booking]:
+ """
+ Create a Booking.
+
+ Args:
+ request_body: The values to create booking with.
+ overrides: The request overrides to use for the request.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created Booking.
+ """
+
+ return super().create(
+ path="/v3/scheduling/bookings",
+ request_body=request_body,
+ query_params=query_params,
+ response_type=Booking,
+ overrides=overrides,
+ )
+
+ def confirm(
+ self,
+ booking_id: str,
+ request_body: ConfirmBookingRequest,
+ query_params: ConfirmBookingQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Booking]:
+ """
+ Confirm a Booking.
+
+ Args:
+ booking_id: The identifier of the Booking to confirm.
+ request_body: The values to confirm booking with.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The confirmed Booking.
+ """
+
+ return super().update(
+ path=f"/v3/scheduling/bookings/{booking_id}",
+ request_body=request_body,
+ query_params=query_params,
+ response_type=Booking,
+ overrides=overrides,
+ )
+
+ def reschedule(
+ self,
+ booking_id: str,
+ request_body: RescheduleBookingRequest,
+ query_params: RescheduleBookingQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Booking]:
+ """
+ Reschedule a Booking.
+
+ Args:
+ booking_id: The identifier of the Booking to reschedule.
+ request_body: The values to reschedule booking with.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The rescheduled Booking.
+ """
+
+ return super().patch(
+ path=f"/v3/scheduling/bookings/{booking_id}",
+ request_body=request_body,
+ query_params=query_params,
+ response_type=Booking,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ booking_id: str,
+ request_body: DeleteBookingRequest,
+ query_params: DestroyBookingQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete a Booking.
+
+ Args:
+ booking_id: The identifier of the Booking to delete.
+ request_body: The reason to delete booking with.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ None.
+ """
+
+ return super().destroy(
+ path=f"/v3/scheduling/bookings/{booking_id}",
+ request_body=request_body,
+ query_params=query_params,
+ overrides=overrides,
+ )
diff --git a/nylas/resources/calendars.py b/nylas/resources/calendars.py
new file mode 100644
index 00000000..971fa22b
--- /dev/null
+++ b/nylas/resources/calendars.py
@@ -0,0 +1,223 @@
+from typing import List
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.availability import GetAvailabilityResponse, GetAvailabilityRequest
+from nylas.models.free_busy import (
+ GetFreeBusyResponse,
+ GetFreeBusyRequest,
+ FreeBusyError,
+ FreeBusy,
+)
+from nylas.models.calendars import (
+ Calendar,
+ CreateCalendarRequest,
+ UpdateCalendarRequest,
+ ListCalendarsQueryParams,
+ FindCalendarQueryParams,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+class Calendars(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Calendar API
+
+ The Nylas calendar API allows you to create new calendars or manage existing ones, as well as getting
+ free/busy information for a calendar and getting availability for a calendar.
+
+ A calendar can be accessed by one, or several people, and can contain events.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: ListCalendarsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Calendar]:
+ """
+ Return all Calendars.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of Calendars.
+ """
+
+ return super().list(
+ path=f"/v3/grants/{identifier}/calendars",
+ query_params=query_params,
+ response_type=Calendar,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ calendar_id: str,
+ overrides: RequestOverrides = None,
+ query_params: FindCalendarQueryParams = None,
+ ) -> Response[Calendar]:
+ """
+ Return a Calendar.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ calendar_id: The ID of the Calendar to retrieve.
+ Use "primary" to refer to the primary Calendar associated with the Grant.
+ overrides: The request overrides to use for the request.
+ query_params: The query parameters to include in the request.
+
+ Returns:
+ The Calendar.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/calendars/{calendar_id}",
+ response_type=Calendar,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateCalendarRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Calendar]:
+ """
+ Create a Calendar.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ request_body: The values to create the Calendar with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created Calendar.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/calendars",
+ response_type=Calendar,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ calendar_id: str,
+ request_body: UpdateCalendarRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Calendar]:
+ """
+ Update a Calendar.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ calendar_id: The ID of the Calendar to update.
+ Use "primary" to refer to the primary Calendar associated with the Grant.
+ request_body: The values to update the Calendar with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The updated Calendar.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/calendars/{calendar_id}",
+ response_type=Calendar,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, identifier: str, calendar_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Calendar.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ calendar_id: The ID of the Calendar to delete.
+ Use "primary" to refer to the primary Calendar associated with the Grant.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/calendars/{calendar_id}", overrides=overrides
+ )
+
+ def get_availability(self, request_body: GetAvailabilityRequest, overrides: RequestOverrides = None
+ ) -> Response[GetAvailabilityResponse]:
+ """
+ Get availability for a Calendar.
+
+ Args:
+ request_body: The request body to send to the API.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ Response: The availability response from the API.
+ """
+ json_response, headers = self._http_client._execute(
+ "POST",
+ "/v3/calendars/availability",
+ None,
+ None,
+ request_body,
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, GetAvailabilityResponse, headers)
+
+ def get_free_busy(
+ self,
+ identifier: str,
+ request_body: GetFreeBusyRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[List[GetFreeBusyResponse]]:
+ """
+ Get free/busy info for a Calendar.
+
+ Args:
+ identifier: The grant ID or email account to get free/busy for.
+ request_body: The request body to send to the API.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ Response: The free/busy response from the API.
+ """
+ json_response, headers = self._http_client._execute(
+ "POST",
+ f"/v3/grants/{identifier}/calendars/free-busy",
+ None,
+ None,
+ request_body,
+ overrides=overrides,
+ )
+
+ data = []
+ request_id = json_response["request_id"]
+ for item in json_response["data"]:
+ if item.get("object") == "error":
+ data.append(FreeBusyError.from_dict(item))
+ else:
+ data.append(FreeBusy.from_dict(item))
+
+ return Response(data, request_id, headers)
diff --git a/nylas/resources/configurations.py b/nylas/resources/configurations.py
new file mode 100644
index 00000000..103b0c54
--- /dev/null
+++ b/nylas/resources/configurations.py
@@ -0,0 +1,159 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+)
+from nylas.models.list_query_params import ListQueryParams
+from nylas.models.response import DeleteResponse, ListResponse, Response
+from nylas.models.scheduler import (
+ Configuration,
+ CreateConfigurationRequest,
+ UpdateConfigurationRequest,
+)
+
+
+class ListConfigurationsParams(ListQueryParams):
+ """
+ Interface of the query parameters for listing configurations.
+
+ Attributes:
+ limit: The maximum number of objects to return.
+ This field defaults to 50. The maximum allowed value is 200.
+ page_token: An identifier that specifies which page of data to return.
+ This value should be taken from a ListResponse object's next_cursor parameter.
+ identifier: The identifier of the Grant to act upon.
+ """
+
+ identifier: str
+
+
+class Configurations(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Configuration API
+
+ The Nylas configuration API allows you to create new configurations or manage existing ones, as well as getting
+ configurations details for a user.
+
+ Nylas Scheduler stores Configuration objects in the Scheduler database and loads
+ them as Scheduling Pages in the Scheduler UI.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: ListConfigurationsParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Configuration]:
+ """
+ Return all Configurations.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of Configurations.
+ """
+ # import pdb; pdb.set_trace();
+ res = super().list(
+ path=f"/v3/grants/{identifier}/scheduling/configurations",
+ overrides=overrides,
+ response_type=Configuration,
+ query_params=query_params,
+ )
+ return res
+
+ def find(
+ self, identifier: str, config_id: str, overrides: RequestOverrides = None
+ ) -> Response[Configuration]:
+ """
+ Return a Configuration.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ config_id: The identifier of the Configuration to get.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Configuration object.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}",
+ overrides=overrides,
+ response_type=Configuration,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateConfigurationRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Configuration]:
+ """
+ Create a new Configuration.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ data: The data to create the Configuration with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Configuration object.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/scheduling/configurations",
+ request_body=request_body,
+ overrides=overrides,
+ response_type=Configuration,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ config_id: str,
+ request_body: UpdateConfigurationRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Configuration]:
+ """
+ Update a Configuration.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ config_id: The identifier of the Configuration to update.
+ data: The data to update the Configuration with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Configuration object.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}",
+ request_body=request_body,
+ overrides=overrides,
+ response_type=Configuration,
+ )
+
+ def destroy(
+ self, identifier: str, config_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Configuration.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ config_id: The identifier of the Configuration to delete.
+ overrides: The request overrides to use for the request.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/scheduling/configurations/{config_id}",
+ overrides=overrides,
+ )
diff --git a/nylas/resources/connectors.py b/nylas/resources/connectors.py
new file mode 100644
index 00000000..7ce217b4
--- /dev/null
+++ b/nylas/resources/connectors.py
@@ -0,0 +1,146 @@
+from nylas.config import RequestOverrides
+from nylas.resources.credentials import Credentials
+
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.auth import Provider
+from nylas.models.connectors import (
+ ListConnectorQueryParams,
+ Connector,
+ CreateConnectorRequest,
+ UpdateConnectorRequest,
+)
+from nylas.models.response import ListResponse, Response, DeleteResponse
+
+
+class Connectors(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Connectors API
+
+ The Nylas Connectors API allows you to create new connectors or manage existing ones.
+ In Nylas, a connector (formerly called an "integration") stores information that allows your Nylas application
+ to connect to a third party services
+ """
+
+ @property
+ def credentials(self) -> Credentials:
+ """
+ Access the Credentials API.
+
+ Returns:
+ The Credentials API.
+ """
+ return Credentials(self._http_client)
+
+ def list(
+ self,
+ query_params: ListConnectorQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Connector]:
+ """
+ Return all Connectors.
+
+ Args:
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use.
+
+ Returns:
+ The list of Connectors.
+ """
+
+ return super().list(
+ path="/v3/connectors",
+ response_type=Connector,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self, provider: Provider, overrides: RequestOverrides = None
+ ) -> Response[Connector]:
+ """
+ Return a connector associated with the provider.
+
+ Args:
+ provider: The provider associated to the connector to retrieve.
+ overrides: The request overrides to use.
+
+ Returns:
+ The Connector.
+ """
+ return super().find(
+ path=f"/v3/connectors/{provider}",
+ response_type=Connector,
+ overrides=overrides,
+ )
+
+ def create(
+ self, request_body: CreateConnectorRequest, overrides: RequestOverrides = None
+ ) -> Response[Connector]:
+ """
+ Create a connector.
+
+ Args:
+ request_body: The values to create the connector with.
+ overrides: The request overrides to use.
+
+ Returns:
+ The created connector.
+ """
+ return super().create(
+ path="/v3/connectors",
+ request_body=request_body,
+ response_type=Connector,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ provider: Provider,
+ request_body: UpdateConnectorRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Connector]:
+ """
+ Create a connector.
+
+ Args:
+ provider: The provider associated to the connector to update.
+ request_body: The values to update the connector with.
+ overrides: The request overrides to use.
+
+ Returns:
+ The created connector.
+ """
+ return super().update(
+ path=f"/v3/connectors/{provider}",
+ request_body=request_body,
+ response_type=Connector,
+ method="PATCH",
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, provider: Provider, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a connector.
+
+ Args:
+ provider: The provider associated to the connector to delete.
+ overrides: The request overrides to use.
+
+ Returns:
+ The deleted connector.
+ """
+ return super().destroy(path=f"/v3/connectors/{provider}", overrides=overrides)
diff --git a/nylas/resources/contacts.py b/nylas/resources/contacts.py
new file mode 100644
index 00000000..de78a9fd
--- /dev/null
+++ b/nylas/resources/contacts.py
@@ -0,0 +1,182 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.contacts import (
+ Contact,
+ CreateContactRequest,
+ UpdateContactRequest,
+ ListContactsQueryParams,
+ FindContactQueryParams,
+ ListContactGroupsQueryParams,
+ ContactGroup,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+class Contacts(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Contacts API
+
+ The Contacts API allows you to manage contacts and contact groups for a user.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: ListContactsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Contact]:
+ """
+ Return all Contacts.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of contacts.
+ """
+
+ return super().list(
+ path=f"/v3/grants/{identifier}/contacts",
+ query_params=query_params,
+ response_type=Contact,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ contact_id: str,
+ query_params: FindContactQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Contact]:
+ """
+ Return a Contact.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ contact_id: The ID of the contact to retrieve.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The contact.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/contacts/{contact_id}",
+ response_type=Contact,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateContactRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Contact]:
+ """
+ Create a Contact.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ request_body: The values to create the Contact with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created contact.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/contacts",
+ response_type=Contact,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ contact_id: str,
+ request_body: UpdateContactRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Contact]:
+ """
+ Update a Contact.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ contact_id: The ID of the Contact to update.
+ Use "primary" to refer to the primary Contact associated with the Grant.
+ request_body: The values to update the Contact with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The updated contact.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/contacts/{contact_id}",
+ response_type=Contact,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ identifier: str,
+ contact_id: str,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete a Contact.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ contact_id: The ID of the Contact to delete.
+ Use "primary" to refer to the primary Contact associated with the Grant.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/contacts/{contact_id}", overrides=overrides
+ )
+
+ def list_groups(
+ self,
+ identifier: str,
+ query_params: ListContactGroupsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[ContactGroup]:
+ """
+ Return all contact groups.
+
+ Attributes:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of contact groups.
+ """
+ json_response, headers = self._http_client._execute(
+ method="GET",
+ path=f"/v3/grants/{identifier}/contacts/groups",
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ return ListResponse.from_dict(json_response, ContactGroup, headers)
diff --git a/nylas/resources/credentials.py b/nylas/resources/credentials.py
new file mode 100644
index 00000000..336cb06c
--- /dev/null
+++ b/nylas/resources/credentials.py
@@ -0,0 +1,156 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.auth import Provider
+from nylas.models.credentials import (
+ Credential,
+ CredentialRequest,
+ ListCredentialQueryParams,
+ UpdateCredentialRequest,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+class Credentials(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Credentials API
+
+
+ A Nylas connector credential is a special type of record that securely stores information
+ that allows you to connect using an administrator account
+ """
+
+ def list(
+ self,
+ provider: Provider,
+ query_params: ListCredentialQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Credential]:
+ """
+ Return all credentials for a particular provider.
+
+ Args:
+ provider: The provider.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of credentials.
+ """
+
+ return super().list(
+ path=f"/v3/connectors/{provider}/creds",
+ response_type=Credential,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ provider: Provider,
+ credential_id: str,
+ overrides: RequestOverrides = None,
+ ) -> Response[Credential]:
+ """
+ Return a credential.
+
+ Args:
+ provider: The provider of the credential.
+ credential_id: The ID of the credential to retrieve.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Credential.
+ """
+
+ return super().find(
+ path=f"/v3/connectors/{provider}/creds/{credential_id}",
+ response_type=Credential,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ provider: Provider,
+ request_body: CredentialRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Credential]:
+ """
+ Create a credential for a particular provider.
+
+ Args:
+ provider: The provider.
+ request_body: The values to create the Credential with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created Credential.
+ """
+
+ return super().create(
+ path=f"/v3/connectors/{provider}/creds",
+ response_type=Credential,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ provider: Provider,
+ credential_id: str,
+ request_body: UpdateCredentialRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Credential]:
+ """
+ Update a credential.
+
+ Args:
+ provider: The provider.
+ credential_id: The ID of the credential to update.
+ request_body: The values to update the credential with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The updated credential.
+ """
+
+ return super().update(
+ path=f"/v3/connectors/{provider}/creds/{credential_id}",
+ response_type=Credential,
+ request_body=request_body,
+ method="PATCH",
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ provider: Provider,
+ credential_id: str,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete a credential.
+
+ Args:
+ provider: the provider for the grant
+ credential_id: The ID of the credential to delete.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The deletion response.
+ """
+
+ return super().destroy(
+ path=f"/v3/connectors/{provider}/creds/{credential_id}", overrides=overrides
+ )
diff --git a/nylas/resources/domains.py b/nylas/resources/domains.py
new file mode 100644
index 00000000..bde153e1
--- /dev/null
+++ b/nylas/resources/domains.py
@@ -0,0 +1,222 @@
+from typing import Optional
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+)
+from nylas.handler.service_account import ServiceAccountSigner
+from nylas.models.domains import (
+ CreateDomainRequest,
+ Domain,
+ DomainVerificationDetails,
+ GetDomainInfoRequest,
+ ListDomainsQueryParams,
+ UpdateDomainRequest,
+ VerifyDomainRequest,
+)
+from nylas.models.response import DeleteResponse, ListResponse, Response
+
+
+def _merge_signer_headers(
+ overrides: Optional[RequestOverrides], signer_headers: Optional[dict]
+) -> Optional[RequestOverrides]:
+ if not signer_headers:
+ return overrides
+ merged: RequestOverrides = dict(overrides) if overrides else {}
+ headers = dict(merged.get("headers") or {})
+ headers.update(signer_headers)
+ merged["headers"] = headers
+ return merged
+
+
+class Domains(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Manage Domains API (``/v3/admin/domains``).
+
+ Organization admin endpoints for registering and verifying email domains used with
+ Transactional Send and Nylas Inbound. Optional :class:`ServiceAccountSigner` adds the
+ required ``X-Nylas-*`` headers; you can also supply those headers via ``RequestOverrides``.
+ """
+
+ def list(
+ self,
+ query_params: Optional[ListDomainsQueryParams] = None,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Domain]:
+ path = "/v3/admin/domains"
+ merged = overrides
+ if signer:
+ hdrs, _ = signer.build_headers("GET", path, None)
+ merged = _merge_signer_headers(overrides, hdrs)
+ return super().list(
+ path=path,
+ response_type=Domain,
+ query_params=query_params,
+ overrides=merged,
+ )
+
+ def create(
+ self,
+ request_body: CreateDomainRequest,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Domain]:
+ path = "/v3/admin/domains"
+ merged = overrides
+ serialized = None
+ body_arg = request_body
+ if signer:
+ hdrs, serialized = signer.build_headers("POST", path, dict(request_body))
+ merged = _merge_signer_headers(overrides, hdrs)
+ if serialized is not None:
+ body_arg = None
+ return super().create(
+ path=path,
+ request_body=body_arg,
+ response_type=Domain,
+ overrides=merged,
+ serialized_json_body=serialized,
+ )
+
+ def find(
+ self,
+ domain_id: str,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Domain]:
+ path = f"/v3/admin/domains/{domain_id}"
+ merged = overrides
+ if signer:
+ hdrs, _ = signer.build_headers("GET", path, None)
+ merged = _merge_signer_headers(overrides, hdrs)
+ return super().find(
+ path=path,
+ response_type=Domain,
+ overrides=merged,
+ )
+
+ def update(
+ self,
+ domain_id: str,
+ request_body: UpdateDomainRequest,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Domain]:
+ path = f"/v3/admin/domains/{domain_id}"
+ merged = overrides
+ serialized = None
+ body_arg = request_body
+ if signer:
+ hdrs, serialized = signer.build_headers("PUT", path, dict(request_body))
+ merged = _merge_signer_headers(overrides, hdrs)
+ if serialized is not None:
+ body_arg = None
+ return super().update(
+ path=path,
+ request_body=body_arg,
+ response_type=Domain,
+ overrides=merged,
+ serialized_json_body=serialized,
+ )
+
+ def destroy(
+ self,
+ domain_id: str,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ path = f"/v3/admin/domains/{domain_id}"
+ merged = overrides
+ if signer:
+ hdrs, _ = signer.build_headers("DELETE", path, None)
+ merged = _merge_signer_headers(overrides, hdrs)
+ return super().destroy(path=path, overrides=merged)
+
+ def get_info(
+ self,
+ domain_id: str,
+ request_body: GetDomainInfoRequest,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[DomainVerificationDetails]:
+ """
+ Return DNS record information and verification status for the given verification type.
+
+ Args:
+ domain_id: The domain ID.
+ request_body: Body with ``type`` (for example ``ownership`` or ``dkim``).
+ signer: Optional service account signer for ``X-Nylas-*`` headers.
+ overrides: Request overrides (for example extra headers).
+
+ Returns:
+ Verification details including required DNS records.
+ """
+ path = f"/v3/admin/domains/{domain_id}/info"
+ body = dict(request_body)
+ merged = overrides
+ serialized = None
+ if signer:
+ hdrs, serialized = signer.build_headers("POST", path, body)
+ merged = _merge_signer_headers(overrides, hdrs)
+ exec_kwargs = {"overrides": merged}
+ if serialized is not None:
+ exec_kwargs["serialized_json_body"] = serialized
+ res, headers = self._http_client._execute(
+ "POST",
+ path,
+ None,
+ None,
+ None if serialized is not None else body,
+ **exec_kwargs,
+ )
+ return Response.from_dict(res, DomainVerificationDetails, headers)
+
+ def verify(
+ self,
+ domain_id: str,
+ request_body: VerifyDomainRequest,
+ signer: Optional[ServiceAccountSigner] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[DomainVerificationDetails]:
+ """
+ Trigger a verification check for the specified DNS record type.
+
+ Args:
+ domain_id: The domain ID.
+ request_body: Body with ``type`` of verification to run.
+ signer: Optional service account signer for ``X-Nylas-*`` headers.
+ overrides: Request overrides (for example extra headers).
+
+ Returns:
+ Verification attempt details and status.
+ """
+ path = f"/v3/admin/domains/{domain_id}/verify"
+ body = dict(request_body)
+ merged = overrides
+ serialized = None
+ if signer:
+ hdrs, serialized = signer.build_headers("POST", path, body)
+ merged = _merge_signer_headers(overrides, hdrs)
+ exec_kwargs = {"overrides": merged}
+ if serialized is not None:
+ exec_kwargs["serialized_json_body"] = serialized
+ res, headers = self._http_client._execute(
+ "POST",
+ path,
+ None,
+ None,
+ None if serialized is not None else body,
+ **exec_kwargs,
+ )
+ return Response.from_dict(res, DomainVerificationDetails, headers)
diff --git a/nylas/resources/drafts.py b/nylas/resources/drafts.py
new file mode 100644
index 00000000..52d94b2b
--- /dev/null
+++ b/nylas/resources/drafts.py
@@ -0,0 +1,232 @@
+import io
+import urllib.parse
+from typing import Optional
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+ CreatableApiResource,
+)
+from nylas.models.drafts import (
+ ListDraftsQueryParams,
+ Draft,
+ UpdateDraftRequest,
+ CreateDraftRequest,
+ FindDraftQueryParams,
+)
+from nylas.models.messages import Message
+from nylas.models.response import ListResponse, Response, DeleteResponse
+from nylas.utils.file_utils import (
+ _build_form_request,
+ MAXIMUM_JSON_ATTACHMENT_SIZE,
+ encode_stream_to_base64,
+)
+
+
+class Drafts(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Draft API
+
+ The Drafts API allows you to create, read, update, and delete drafts and send them as messages.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: Optional[ListDraftsQueryParams] = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Draft]:
+ """
+ Return all Drafts.
+
+ Args:
+ identifier: The identifier of the grant to get drafts for.
+ query_params: The query parameters to filter drafts by.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ A list of Drafts.
+ """
+ return super().list(
+ path=f"/v3/grants/{identifier}/drafts",
+ response_type=Draft,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ draft_id: str,
+ overrides: RequestOverrides = None,
+ query_params: FindDraftQueryParams = None,
+ ) -> Response[Draft]:
+ """
+ Return a Draft.
+
+ Args:
+ identifier: The identifier of the grant to get the draft for.
+ draft_id: The identifier of the draft to get.
+ overrides: The request overrides to use for the request.
+ query_params: The query parameters to include in the request.
+
+ Returns:
+ The requested Draft.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/drafts/{urllib.parse.quote(draft_id, safe='')}",
+ response_type=Draft,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateDraftRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Draft]:
+ """
+ Create a Draft.
+
+ Args:
+ identifier: The identifier of the grant to send the message for.
+ request_body: The request body to create a draft with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The newly created Draft.
+ """
+ path = f"/v3/grants/{identifier}/drafts"
+
+ # Use form data only if the attachment size is greater than 3mb
+ attachment_size = sum(
+ attachment.get("size", 0)
+ for attachment in request_body.get("attachments", [])
+ )
+ if attachment_size >= MAXIMUM_JSON_ATTACHMENT_SIZE:
+ json_response = self._http_client._execute(
+ method="POST",
+ path=path,
+ data=_build_form_request(request_body),
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, Draft)
+
+ # Encode the content of the attachments to base64
+ for attachment in request_body.get("attachments", []):
+ if "content" in attachment and issubclass(
+ type(attachment["content"]), io.IOBase
+ ):
+ attachment["content"] = encode_stream_to_base64(attachment["content"])
+
+ return super().create(
+ path=path,
+ response_type=Draft,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ draft_id: str,
+ request_body: UpdateDraftRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Draft]:
+ """
+ Update a Draft.
+
+ Args:
+ identifier: The identifier of the grant to update the draft for.
+ draft_id: The identifier of the draft to update.
+ request_body: The request body to update the draft with.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The updated Draft.
+ """
+ path = f"/v3/grants/{identifier}/drafts/{urllib.parse.quote(draft_id, safe='')}"
+
+ # Use form data only if the attachment size is greater than 3mb
+ attachment_size = sum(
+ attachment.get("size", 0)
+ for attachment in request_body.get("attachments", [])
+ )
+ if attachment_size >= MAXIMUM_JSON_ATTACHMENT_SIZE:
+ json_response = self._http_client._execute(
+ method="PUT",
+ path=path,
+ data=_build_form_request(request_body),
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, Draft)
+
+ # Encode the content of the attachments to base64
+ for attachment in request_body.get("attachments", []):
+ if "content" in attachment and issubclass(
+ type(attachment["content"]), io.IOBase
+ ):
+ attachment["content"] = encode_stream_to_base64(attachment["content"])
+
+ return super().update(
+ path=path,
+ response_type=Draft,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ identifier: str,
+ draft_id: str,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete a Draft.
+
+ Args:
+ identifier: The identifier of the grant to delete the draft for.
+ draft_id: The identifier of the draft to delete.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/drafts/{urllib.parse.quote(draft_id, safe='')}",
+ overrides=overrides,
+ )
+
+ def send(
+ self,
+ identifier: str,
+ draft_id: str,
+ overrides: RequestOverrides = None,
+ ) -> Response[Message]:
+ """
+ Send a Draft.
+
+ Args:
+ identifier: The identifier of the grant to send the draft for.
+ draft_id: The identifier of the draft to send.
+ overrides: The request overrides to use for the request.
+ """
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path=f"/v3/grants/{identifier}/drafts/{urllib.parse.quote(draft_id, safe='')}",
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, Message, headers)
diff --git a/nylas/resources/events.py b/nylas/resources/events.py
new file mode 100644
index 00000000..3b9223ab
--- /dev/null
+++ b/nylas/resources/events.py
@@ -0,0 +1,236 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.events import (
+ Event,
+ UpdateEventRequest,
+ CreateEventRequest,
+ FindEventQueryParams,
+ ListEventQueryParams,
+ ListImportEventsQueryParams,
+ CreateEventQueryParams,
+ UpdateEventQueryParams,
+ DestroyEventQueryParams,
+ SendRsvpQueryParams,
+ SendRsvpRequest,
+)
+from nylas.models.response import (
+ Response,
+ ListResponse,
+ DeleteResponse,
+ RequestIdOnlyResponse,
+)
+
+
+class Events(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Events API
+
+ The Events API allows you to find, create, update, and delete events on any calendar on your Nylas account.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: ListEventQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Event]:
+ """
+ Return all Events.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of Events.
+ """
+
+ return super().list(
+ path=f"/v3/grants/{identifier}/events",
+ response_type=Event,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def list_import_events(
+ self,
+ identifier: str,
+ query_params: ListImportEventsQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Event]:
+ """
+ Returns a list of recurring events, recurring event exceptions, and
+ single events from the specified calendar within a given time frame.
+ This is useful when you want to import, store, and synchronize events
+ from the time frame to your application
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The list of imported Events.
+ """
+
+ return super().list(
+ path=f"/v3/grants/{identifier}/events/import",
+ response_type=Event,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ event_id: str,
+ query_params: FindEventQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> Response[Event]:
+ """
+ Return an Event.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ event_id: The ID of the Event to retrieve.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Event.
+ """
+
+ return super().find(
+ path=f"/v3/grants/{identifier}/events/{event_id}",
+ response_type=Event,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateEventRequest,
+ query_params: CreateEventQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> Response[Event]:
+ """
+ Create an Event.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ request_body: The values to create the Event with.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The created Event.
+ """
+
+ return super().create(
+ path=f"/v3/grants/{identifier}/events",
+ response_type=Event,
+ request_body=request_body,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ event_id: str,
+ request_body: UpdateEventRequest,
+ query_params: UpdateEventQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> Response[Event]:
+ """
+ Update an Event.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ event_id: The ID of the Event to update.
+ request_body: The values to update the Event with.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The updated Event.
+ """
+
+ return super().update(
+ path=f"/v3/grants/{identifier}/events/{event_id}",
+ response_type=Event,
+ request_body=request_body,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ identifier: str,
+ event_id: str,
+ query_params: DestroyEventQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete an Event.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ event_id: The ID of the Event to delete.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The deletion response.
+ """
+
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/events/{event_id}",
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def send_rsvp(
+ self,
+ identifier: str,
+ event_id: str,
+ request_body: SendRsvpRequest,
+ query_params: SendRsvpQueryParams,
+ overrides: RequestOverrides = None,
+ ) -> RequestIdOnlyResponse:
+ """
+ Send an RSVP for an event.
+
+ Args:
+ identifier: The grant ID or email account to send RSVP for.
+ event_id: The event ID to send RSVP for.
+ query_params: The query parameters to send to the API.
+ request_body: The request body to send to the API.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ Response: The RSVP response from the API.
+ """
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path=f"/v3/grants/{identifier}/events/{event_id}/send-rsvp",
+ query_params=query_params,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ return RequestIdOnlyResponse.from_dict(json_response, headers)
diff --git a/nylas/resources/folders.py b/nylas/resources/folders.py
new file mode 100644
index 00000000..487d7ea3
--- /dev/null
+++ b/nylas/resources/folders.py
@@ -0,0 +1,154 @@
+from typing import Optional
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.folders import (
+ Folder,
+ CreateFolderRequest,
+ UpdateFolderRequest,
+ ListFolderQueryParams,
+ FindFolderQueryParams,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+class Folders(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Folders API
+
+ The Nylas folders API allows you to create new folders or manage existing ones.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: Optional[ListFolderQueryParams] = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Folder]:
+ """
+ Return all Folders.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use.
+
+ Returns:
+ The list of Folders.
+ """
+
+ return super().list(
+ path=f"/v3/grants/{identifier}/folders",
+ response_type=Folder,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ folder_id: str,
+ overrides: RequestOverrides = None,
+ query_params: FindFolderQueryParams = None,
+ ) -> Response[Folder]:
+ """
+ Return a Folder.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ folder_id: The ID of the Folder to retrieve.
+ overrides: The request overrides to use.
+ query_params: The query parameters to include in the request.
+
+ Returns:
+ The Folder.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/folders/{folder_id}",
+ response_type=Folder,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ identifier: str,
+ request_body: CreateFolderRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Folder]:
+ """
+ Create a Folder.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ request_body: The values to create the Folder with.
+ overrides: The request overrides to use.
+
+ Returns:
+ The created Folder.
+ """
+ return super().create(
+ path=f"/v3/grants/{identifier}/folders",
+ response_type=Folder,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ folder_id: str,
+ request_body: UpdateFolderRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Folder]:
+ """
+ Update a Folder.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ folder_id: The ID of the Folder to update.
+ request_body: The values to update the Folder with.
+ overrides: The request overrides to use.
+
+ Returns:
+ The updated Folder.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/folders/{folder_id}",
+ response_type=Folder,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ identifier: str,
+ folder_id: str,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Delete a Folder.
+
+ Args:
+ identifier: The identifier of the Grant to act upon.
+ folder_id: The ID of the Folder to delete.
+ overrides: The request overrides to use.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/folders/{folder_id}", overrides=overrides
+ )
diff --git a/nylas/resources/grants.py b/nylas/resources/grants.py
new file mode 100644
index 00000000..4a7675b6
--- /dev/null
+++ b/nylas/resources/grants.py
@@ -0,0 +1,134 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.grants import (
+ Grant,
+ ListGrantsQueryParams,
+ UpdateGrantRequest,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+def _normalize_grants_query_params(query_params: ListGrantsQueryParams = None) -> dict:
+ if not query_params:
+ return query_params
+
+ normalized_query_params = dict(query_params)
+ key_aliases = {
+ "sortBy": "sort_by",
+ "orderBy": "order_by",
+ "grantStatus": "grant_status",
+ }
+
+ for camel_case_key, snake_case_key in key_aliases.items():
+ if camel_case_key in normalized_query_params:
+ if snake_case_key not in normalized_query_params:
+ normalized_query_params[snake_case_key] = normalized_query_params[
+ camel_case_key
+ ]
+ del normalized_query_params[camel_case_key]
+
+ return normalized_query_params
+
+
+class Grants(
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Grants API
+
+ The Grants API allows you to find and manage existing grants for your Nylas application.
+
+ Grants represent a specific set of permissions ("scopes") that a specific end user granted Nylas
+ for a specific service provider
+ """
+
+ def list(
+ self,
+ query_params: ListGrantsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Grant]:
+ """
+ Return all Grants.
+
+ Args:
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to use.
+
+ Returns:
+ A list of Grants.
+ """
+
+ return super().list(
+ path="/v3/grants",
+ response_type=Grant,
+ query_params=_normalize_grants_query_params(query_params),
+ overrides=overrides,
+ )
+
+ def find(
+ self, grant_id: str, overrides: RequestOverrides = None
+ ) -> Response[Grant]:
+ """
+ Return a Grant.
+
+ Args:
+ grant_id: The ID of the Grant to retrieve.
+ overrides: The request overrides to use.
+
+ Returns:
+ The Grant.
+ """
+
+ return super().find(
+ path=f"/v3/grants/{grant_id}", response_type=Grant, overrides=overrides
+ )
+
+ def update(
+ self,
+ grant_id: str,
+ request_body: UpdateGrantRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Grant]:
+ """
+ Update a Grant.
+
+ Args:
+ grant_id: The ID of the Grant to update.
+ request_body: The values to update the Grant with.
+ overrides: The request overrides to use.
+
+ Returns:
+ The updated Grant.
+ """
+
+ return super().update(
+ path=f"/v3/grants/{grant_id}",
+ response_type=Grant,
+ request_body=request_body,
+ overrides=overrides,
+ method="PATCH"
+ )
+
+ def destroy(
+ self, grant_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Grant.
+
+ Args:
+ grant_id: The ID of the Grant to delete.
+ overrides: The request overrides to use.
+
+ Returns:
+ The deletion response.
+ """
+
+ return super().destroy(path=f"/v3/grants/{grant_id}", overrides=overrides)
diff --git a/nylas/resources/lists.py b/nylas/resources/lists.py
new file mode 100644
index 00000000..aa28cea5
--- /dev/null
+++ b/nylas/resources/lists.py
@@ -0,0 +1,126 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+)
+from nylas.models.lists import (
+ CreateListRequest,
+ ListItem,
+ ListListItemsQueryParams,
+ ListListsQueryParams,
+ NylasList,
+ UpdateListItemsRequest,
+ UpdateListRequest,
+)
+from nylas.models.response import DeleteResponse, ListResponse, Response
+
+
+class Lists(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """Nylas Lists API."""
+
+ def list(
+ self,
+ query_params: ListListsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[NylasList]:
+ """Return all lists for the application."""
+ return super().list(
+ path="/v3/lists",
+ response_type=NylasList,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ request_body: CreateListRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[NylasList]:
+ """Create a new list."""
+ return super().create(
+ path="/v3/lists",
+ request_body=request_body,
+ response_type=NylasList,
+ overrides=overrides,
+ )
+
+ def find(self, list_id: str, overrides: RequestOverrides = None) -> Response[NylasList]:
+ """Return a specific list by ID."""
+ return super().find(
+ path=f"/v3/lists/{list_id}",
+ response_type=NylasList,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ list_id: str,
+ request_body: UpdateListRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[NylasList]:
+ """Update a list by ID."""
+ return super().update(
+ path=f"/v3/lists/{list_id}",
+ response_type=NylasList,
+ request_body=request_body,
+ method="PUT",
+ overrides=overrides,
+ )
+
+ def destroy(self, list_id: str, overrides: RequestOverrides = None) -> DeleteResponse:
+ """Delete a list by ID."""
+ return super().destroy(path=f"/v3/lists/{list_id}", overrides=overrides)
+
+ def list_items(
+ self,
+ list_id: str,
+ query_params: ListListItemsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[ListItem]:
+ """Return all items in a list."""
+ return super().list(
+ path=f"/v3/lists/{list_id}/items",
+ response_type=ListItem,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def add_items(
+ self,
+ list_id: str,
+ request_body: UpdateListItemsRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[NylasList]:
+ """Add items to a list."""
+ return super().create(
+ path=f"/v3/lists/{list_id}/items",
+ request_body=request_body,
+ response_type=NylasList,
+ overrides=overrides,
+ )
+
+ def remove_items(
+ self,
+ list_id: str,
+ request_body: UpdateListItemsRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[NylasList]:
+ """Remove items from a list."""
+ json_response, headers = self._http_client._execute(
+ "DELETE",
+ f"/v3/lists/{list_id}/items",
+ None,
+ None,
+ request_body,
+ overrides=overrides,
+ )
+ return Response.from_dict(json_response, NylasList, headers)
diff --git a/nylas/resources/messages.py b/nylas/resources/messages.py
new file mode 100644
index 00000000..1e0b1fb0
--- /dev/null
+++ b/nylas/resources/messages.py
@@ -0,0 +1,302 @@
+import io
+import urllib.parse
+from typing import Optional, List
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.drafts import SendMessageRequest
+from nylas.models.messages import (
+ Message,
+ ListMessagesQueryParams,
+ FindMessageQueryParams,
+ UpdateMessageRequest,
+ ScheduledMessage,
+ StopScheduledMessageResponse,
+ CleanMessagesRequest,
+ CleanMessagesResponse,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+from nylas.resources.smart_compose import SmartCompose
+from nylas.utils.file_utils import (
+ _build_form_request,
+ MAXIMUM_JSON_ATTACHMENT_SIZE,
+ encode_stream_to_base64,
+)
+
+
+class Messages(
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Messages API
+
+ The messages API allows you to send, find, update, and delete messages.
+ You can also use the messages API to schedule messages to be sent at a later time.
+ The Smart Compose API, allowing you to generate email content using machine learning, is also available.
+ """
+
+ @property
+ def smart_compose(self) -> SmartCompose:
+ """
+ Access the Smart Compose collection of endpoints.
+
+ Returns:
+ The Smart Compose collection of endpoints.
+ """
+ return SmartCompose(self._http_client)
+
+ def list(
+ self,
+ identifier: str,
+ query_params: Optional[ListMessagesQueryParams] = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Message]:
+ """
+ Return all Messages.
+
+ Args:
+ identifier: The identifier of the grant to get messages for.
+ query_params: The query parameters to filter messages by.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ A list of Messages.
+ """
+ return super().list(
+ path=f"/v3/grants/{identifier}/messages",
+ response_type=Message,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ identifier: str,
+ message_id: str,
+ query_params: Optional[FindMessageQueryParams] = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Message]:
+ """
+ Return a Message.
+
+ Args:
+ identifier: The identifier of the grant to get the message for.
+ message_id: The identifier of the message to get.
+ query_params: The query parameters to include in the request.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The requested Message.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/messages/{urllib.parse.quote(message_id, safe='')}",
+ response_type=Message,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ message_id: str,
+ request_body: UpdateMessageRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Message]:
+ """
+ Update a Message.
+
+ Args:
+ identifier: The identifier of the grant to update the message for.
+ message_id: The identifier of the message to update.
+ request_body: The request body to update the message with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The updated Message.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/messages/{urllib.parse.quote(message_id, safe='')}",
+ response_type=Message,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, identifier: str, message_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Message.
+
+ Args:
+ identifier: The identifier of the grant to delete the message for.
+ message_id: The identifier of the message to delete.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/messages/{urllib.parse.quote(message_id, safe='')}",
+ overrides=overrides,
+ )
+
+ def send(
+ self,
+ identifier: str,
+ request_body: SendMessageRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Message]:
+ """
+ Send a Message.
+
+ Args:
+ identifier: The identifier of the grant to send the message for.
+ request_body: The request body to send the message with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The sent message.
+ """
+ path = f"/v3/grants/{identifier}/messages/send"
+ form_data = None
+ json_body = None
+
+ # From is a reserved keyword in Python, so we need to pull the data from 'from_' instead
+ # Handle both dictionary-style "from" and typed "from_" field
+ if "from_" in request_body and "from" not in request_body:
+ request_body["from"] = request_body["from_"]
+ del request_body["from_"]
+ # If "from" already exists, leave it unchanged
+
+ # Use form data only if the attachment size is greater than 3mb
+ attachment_size = sum(
+ attachment.get("size", 0)
+ for attachment in request_body.get("attachments", [])
+ )
+ if attachment_size >= MAXIMUM_JSON_ATTACHMENT_SIZE:
+ form_data = _build_form_request(request_body)
+ else:
+ # Encode the content of the attachments to base64
+ for attachment in request_body.get("attachments", []):
+ if "content" in attachment and issubclass(
+ type(attachment["content"]), io.IOBase
+ ):
+ attachment["content"] = encode_stream_to_base64(
+ attachment["content"]
+ )
+
+ json_body = request_body
+
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path=path,
+ request_body=json_body,
+ data=form_data,
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, Message, headers)
+
+ def list_scheduled_messages(
+ self, identifier: str, overrides: RequestOverrides = None
+ ) -> Response[List[ScheduledMessage]]:
+ """
+ Retrieve your scheduled messages.
+
+ Args:
+ identifier: The identifier of the grant to delete the message for.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ Response: The list of scheduled messages.
+ """
+ json_response, headers = self._http_client._execute(
+ method="GET",
+ path=f"/v3/grants/{identifier}/messages/schedules",
+ overrides=overrides,
+ )
+
+ data = []
+ request_id = json_response["request_id"]
+ for item in json_response["data"]:
+ data.append(ScheduledMessage.from_dict(item))
+
+ return Response(data, request_id, headers)
+
+ def find_scheduled_message(
+ self, identifier: str, schedule_id: str, overrides: RequestOverrides = None
+ ) -> Response[ScheduledMessage]:
+ """
+ Retrieve your scheduled messages.
+
+ Args:
+ identifier: The identifier of the grant to delete the message for.
+ schedule_id: The id of the scheduled message to retrieve.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ Response: The scheduled message.
+ """
+ json_response, headers = self._http_client._execute(
+ method="GET",
+ path=f"/v3/grants/{identifier}/messages/schedules/{schedule_id}",
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, ScheduledMessage, headers)
+
+ def stop_scheduled_message(
+ self, identifier: str, schedule_id: str, overrides: RequestOverrides = None
+ ) -> Response[StopScheduledMessageResponse]:
+ """
+ Stop a scheduled message.
+
+ Args:
+ identifier: The identifier of the grant to delete the message for.
+ schedule_id: The id of the scheduled message to stop.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ Response: The confirmation of the stopped scheduled message.
+ """
+ json_response, headers = self._http_client._execute(
+ method="DELETE",
+ path=f"/v3/grants/{identifier}/messages/schedules/{schedule_id}",
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, StopScheduledMessageResponse, headers)
+
+ def clean_messages(
+ self,
+ identifier: str,
+ request_body: CleanMessagesRequest,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[CleanMessagesResponse]:
+ """
+ Remove extra information from a list of messages.
+
+ Args:
+ identifier: The identifier of the grant to clean the message for.
+ request_body: The values to clean the message with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ ListResponse: The list of cleaned messages.
+ """
+ json_response, headers = self._http_client._execute(
+ method="PUT",
+ path=f"/v3/grants/{identifier}/messages/clean",
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ return ListResponse.from_dict(json_response, CleanMessagesResponse, headers)
diff --git a/nylas/resources/notetakers.py b/nylas/resources/notetakers.py
new file mode 100644
index 00000000..275025ea
--- /dev/null
+++ b/nylas/resources/notetakers.py
@@ -0,0 +1,236 @@
+from typing import Optional
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatablePatchApiResource)
+from nylas.models.notetakers import (FindNotetakerQueryParams,
+ InviteNotetakerRequest,
+ ListNotetakerQueryParams,
+ Notetaker, NotetakerMedia,
+ NotetakerLeaveResponse,
+ UpdateNotetakerRequest)
+from nylas.models.response import DeleteResponse, ListResponse, Response
+
+
+class Notetakers(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatablePatchApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Notetakers API
+
+ The Nylas Notetakers API allows you to invite Notetaker bots to meetings and manage their status.
+ Notetaker states are represented by the NotetakerState enum, and meeting providers by the MeetingProvider enum.
+ """
+
+ def list(
+ self,
+ identifier: str = None,
+ query_params: Optional[ListNotetakerQueryParams] = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Notetaker]:
+ """
+ Return all Notetakers.
+
+ Args:
+ identifier: The identifier of the Grant to act upon. Optional.
+ query_params: The query parameters to include in the request.
+ You can use NotetakerState enum values for the state parameter:
+ e.g., {"state": NotetakerState.SCHEDULED.value}
+ overrides: The request overrides to use.
+
+ Returns:
+ The list of Notetakers.
+ """
+ path = (
+ "/v3/notetakers"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers"
+ )
+ return super().list(
+ path=path,
+ response_type=Notetaker,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self,
+ notetaker_id: str,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ query_params: FindNotetakerQueryParams = None,
+ ) -> Response[Notetaker]:
+ """
+ Return a Notetaker.
+
+ Args:
+ notetaker_id: The ID of the Notetaker to retrieve.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+ query_params: The query parameters to include in the request.
+
+ Returns:
+ The Notetaker with properties like state (NotetakerState) and meeting_provider (MeetingProvider).
+ """
+ path = (
+ f"/v3/notetakers/{notetaker_id}"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers/{notetaker_id}"
+ )
+ return super().find(
+ path=path,
+ response_type=Notetaker,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def invite(
+ self,
+ request_body: InviteNotetakerRequest,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Notetaker]:
+ """
+ Invite a Notetaker to a meeting.
+
+ Args:
+ request_body: The values to create the Notetaker with.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+
+ Returns:
+ The created Notetaker with state set to NotetakerState.SCHEDULED.
+ """
+ path = (
+ "/v3/notetakers"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers"
+ )
+ return super().create(
+ path=path,
+ response_type=Notetaker,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ notetaker_id: str,
+ request_body: UpdateNotetakerRequest,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[Notetaker]:
+ """
+ Update a Notetaker.
+
+ Args:
+ notetaker_id: The ID of the Notetaker to update.
+ request_body: The values to update the Notetaker with.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+
+ Returns:
+ The updated Notetaker.
+ """
+ path = (
+ f"/v3/notetakers/{notetaker_id}"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers/{notetaker_id}"
+ )
+ return super().patch(
+ path=path,
+ response_type=Notetaker,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def leave(
+ self,
+ notetaker_id: str,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[NotetakerLeaveResponse]:
+ """
+ Remove Notetaker from a meeting.
+
+ Args:
+ notetaker_id: The ID of the Notetaker to remove from the meeting.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+
+ Returns:
+ The response with information about the Notetaker that left,
+ including the Notetaker ID and a message.
+ """
+ path = (
+ f"/v3/notetakers/{notetaker_id}/leave"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers/{notetaker_id}/leave"
+ )
+ return super().create(
+ path=path,
+ response_type=NotetakerLeaveResponse,
+ overrides=overrides,
+ )
+
+ def get_media(
+ self,
+ notetaker_id: str,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ ) -> Response[NotetakerMedia]:
+ """
+ Download Notetaker media.
+
+ Args:
+ notetaker_id: The ID of the Notetaker to get media from.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+
+ Returns:
+ The Notetaker media information including URLs for recordings and transcripts.
+ """
+ path = (
+ f"/v3/notetakers/{notetaker_id}/media"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers/{notetaker_id}/media"
+ )
+ return super().find(
+ path=path,
+ response_type=NotetakerMedia,
+ overrides=overrides,
+ )
+
+ def cancel(
+ self,
+ notetaker_id: str,
+ identifier: str = None,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Cancel a scheduled Notetaker.
+
+ Args:
+ notetaker_id: The ID of the Notetaker to cancel.
+ identifier: The identifier of the Grant to act upon. Optional.
+ overrides: The request overrides to use.
+
+ Returns:
+ The deletion response.
+ """
+ path = (
+ f"/v3/notetakers/{notetaker_id}/cancel"
+ if identifier is None
+ else f"/v3/grants/{identifier}/notetakers/{notetaker_id}/cancel"
+ )
+ return super().destroy(
+ path=path,
+ overrides=overrides,
+ )
diff --git a/nylas/resources/policies.py b/nylas/resources/policies.py
new file mode 100644
index 00000000..88c8ed29
--- /dev/null
+++ b/nylas/resources/policies.py
@@ -0,0 +1,81 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+)
+from nylas.models.policies import (
+ CreatePolicyRequest,
+ ListPoliciesQueryParams,
+ Policy,
+ UpdatePolicyRequest,
+)
+from nylas.models.response import DeleteResponse, ListResponse, Response
+
+
+class Policies(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Policies API.
+
+ Policies define operational configuration for Nylas Agent Accounts.
+ """
+
+ def list(
+ self,
+ query_params: ListPoliciesQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Policy]:
+ return super().list(
+ path="/v3/policies",
+ response_type=Policy,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ request_body: CreatePolicyRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Policy]:
+ return super().create(
+ path="/v3/policies",
+ request_body=request_body,
+ response_type=Policy,
+ overrides=overrides,
+ )
+
+ def find(
+ self, policy_id: str, overrides: RequestOverrides = None
+ ) -> Response[Policy]:
+ return super().find(
+ path=f"/v3/policies/{policy_id}",
+ response_type=Policy,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ policy_id: str,
+ request_body: UpdatePolicyRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Policy]:
+ return super().update(
+ path=f"/v3/policies/{policy_id}",
+ response_type=Policy,
+ request_body=request_body,
+ method="PUT",
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, policy_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ return super().destroy(path=f"/v3/policies/{policy_id}", overrides=overrides)
diff --git a/nylas/resources/redirect_uris.py b/nylas/resources/redirect_uris.py
new file mode 100644
index 00000000..b2831afc
--- /dev/null
+++ b/nylas/resources/redirect_uris.py
@@ -0,0 +1,130 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.redirect_uri import (
+ RedirectUri,
+ CreateRedirectUriRequest,
+ UpdateRedirectUriRequest,
+)
+from nylas.models.response import Response, ListResponse, DeleteResponse
+
+
+class RedirectUris(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Manage Redirect URIs for your Nylas Application.
+
+ These endpoints allow you to create, update, and delete Redirect URIs for your Nylas Application.
+ """
+
+ def list(self, overrides: RequestOverrides = None) -> ListResponse[RedirectUri]:
+ """
+ Return all Redirect URIs.
+
+ Args:
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The list of Redirect URIs.
+ """
+
+ return super().list(
+ path="/v3/applications/redirect-uris",
+ response_type=RedirectUri,
+ overrides=overrides,
+ )
+
+ def find(
+ self, redirect_uri_id: str, overrides: RequestOverrides = None
+ ) -> Response[RedirectUri]:
+ """
+ Return a Redirect URI.
+
+ Args:
+ redirect_uri_id: The ID of the Redirect URI to retrieve.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The Redirect URI.
+ """
+
+ return super().find(
+ path=f"/v3/applications/redirect-uris/{redirect_uri_id}",
+ response_type=RedirectUri,
+ overrides=overrides,
+ )
+
+ def create(
+ self, request_body: CreateRedirectUriRequest, overrides: RequestOverrides = None
+ ) -> Response[RedirectUri]:
+ """
+ Create a Redirect URI.
+
+ Args:
+ request_body: The values to create the Redirect URI with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The created Redirect URI.
+ """
+
+ return super().create(
+ path="/v3/applications/redirect-uris",
+ request_body=request_body,
+ response_type=RedirectUri,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ redirect_uri_id: str,
+ request_body: UpdateRedirectUriRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[RedirectUri]:
+ """
+ Update a Redirect URI.
+
+ Args:
+ redirect_uri_id: The ID of the Redirect URI to update.
+ request_body: The values to update the Redirect URI with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The updated Redirect URI.
+ """
+
+ return super().update(
+ path=f"/v3/applications/redirect-uris/{redirect_uri_id}",
+ request_body=request_body,
+ response_type=RedirectUri,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, redirect_uri_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Redirect URI.
+
+ Args:
+ redirect_uri_id: The ID of the Redirect URI to delete.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The deletion response.
+ """
+
+ return super().destroy(
+ path=f"/v3/applications/redirect-uris/{redirect_uri_id}",
+ overrides=overrides,
+ )
diff --git a/nylas/resources/resource.py b/nylas/resources/resource.py
new file mode 100644
index 00000000..5880fef7
--- /dev/null
+++ b/nylas/resources/resource.py
@@ -0,0 +1,8 @@
+from nylas.handler.http_client import HttpClient
+
+
+class Resource:
+ """Base class for all Nylas API resources."""
+
+ def __init__(self, http_client: HttpClient):
+ self._http_client = http_client
diff --git a/nylas/resources/rules.py b/nylas/resources/rules.py
new file mode 100644
index 00000000..698831d1
--- /dev/null
+++ b/nylas/resources/rules.py
@@ -0,0 +1,94 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ CreatableApiResource,
+ DestroyableApiResource,
+ FindableApiResource,
+ ListableApiResource,
+ UpdatableApiResource,
+)
+from nylas.models.response import DeleteResponse, ListResponse, Response
+from nylas.models.rules import (
+ CreateRuleRequest,
+ ListRuleEvaluationsQueryParams,
+ ListRulesQueryParams,
+ Rule,
+ RuleEvaluation,
+ UpdateRuleRequest,
+)
+
+
+class Rules(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """Nylas Rules API."""
+
+ def list(
+ self,
+ query_params: ListRulesQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Rule]:
+ """Return all rules for the application."""
+ return super().list(
+ path="/v3/rules",
+ response_type=Rule,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def create(
+ self,
+ request_body: CreateRuleRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Rule]:
+ """Create a new rule."""
+ return super().create(
+ path="/v3/rules",
+ request_body=request_body,
+ response_type=Rule,
+ overrides=overrides,
+ )
+
+ def find(self, rule_id: str, overrides: RequestOverrides = None) -> Response[Rule]:
+ """Return a specific rule by ID."""
+ return super().find(
+ path=f"/v3/rules/{rule_id}",
+ response_type=Rule,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ rule_id: str,
+ request_body: UpdateRuleRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Rule]:
+ """Update a rule by ID."""
+ return super().update(
+ path=f"/v3/rules/{rule_id}",
+ response_type=Rule,
+ request_body=request_body,
+ method="PUT",
+ overrides=overrides,
+ )
+
+ def destroy(self, rule_id: str, overrides: RequestOverrides = None) -> DeleteResponse:
+ """Delete a rule by ID."""
+ return super().destroy(path=f"/v3/rules/{rule_id}", overrides=overrides)
+
+ def list_evaluations(
+ self,
+ grant_id: str,
+ query_params: ListRuleEvaluationsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[RuleEvaluation]:
+ """Return rule evaluation audit records for a grant."""
+ return super().list(
+ path=f"/v3/grants/{grant_id}/rule-evaluations",
+ response_type=RuleEvaluation,
+ query_params=query_params,
+ overrides=overrides,
+ )
diff --git a/nylas/resources/scheduler.py b/nylas/resources/scheduler.py
new file mode 100644
index 00000000..e337de46
--- /dev/null
+++ b/nylas/resources/scheduler.py
@@ -0,0 +1,42 @@
+from nylas.resources.bookings import Bookings
+from nylas.resources.configurations import Configurations
+from nylas.resources.sessions import Sessions
+
+
+class Scheduler:
+ """
+ Class representation of a Nylas Scheduler API.
+ """
+
+ def __init__(self, http_client):
+ self.http_client = http_client
+
+ @property
+ def configurations(self) -> Configurations:
+ """
+ Access the Configurations API.
+
+ Returns:
+ The Configurations API.
+ """
+ return Configurations(self.http_client)
+
+ @property
+ def bookings(self) -> Bookings:
+ """
+ Access the Bookings API.
+
+ Returns:
+ The Bookings API.
+ """
+ return Bookings(self.http_client)
+
+ @property
+ def sessions(self) -> Sessions:
+ """
+ Access the Sessions API.
+
+ Returns:
+ The Sessions API.
+ """
+ return Sessions(self.http_client)
diff --git a/nylas/resources/sessions.py b/nylas/resources/sessions.py
new file mode 100644
index 00000000..556009a4
--- /dev/null
+++ b/nylas/resources/sessions.py
@@ -0,0 +1,56 @@
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import CreatableApiResource, DestroyableApiResource
+from nylas.models.response import DeleteResponse, Response
+from nylas.models.scheduler import CreateSessionRequest, Session
+
+
+class Sessions(CreatableApiResource, DestroyableApiResource):
+ """
+ Nylas Sessions API
+
+ The Nylas Sessions API allows you to create new sessions or manage existing ones.
+ """
+
+ def create(
+ self,
+ request_body: CreateSessionRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Session]:
+ """
+ Create a Session.
+
+ Args:
+ request_body: The request body to create the Session.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ The Session.
+ """
+
+ return super().create(
+ path="/v3/scheduling/sessions",
+ request_body=request_body,
+ response_type=Session,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self,
+ session_id: str,
+ overrides: RequestOverrides = None,
+ ) -> DeleteResponse:
+ """
+ Destroy a Session.
+
+ Args:
+ session_id: The identifier of the Session to destroy.
+ overrides: The request overrides to use for the request.
+
+ Returns:
+ None.
+ """
+
+ return super().destroy(
+ path=f"/v3/scheduling/sessions/{session_id}",
+ overrides=overrides,
+ )
diff --git a/nylas/resources/smart_compose.py b/nylas/resources/smart_compose.py
new file mode 100644
index 00000000..1d266d89
--- /dev/null
+++ b/nylas/resources/smart_compose.py
@@ -0,0 +1,67 @@
+from nylas.config import RequestOverrides
+from nylas.models.response import Response
+
+from nylas.models.smart_compose import ComposeMessageRequest, ComposeMessageResponse
+from nylas.resources.resource import Resource
+
+
+class SmartCompose(Resource):
+ """
+ A collection of Smart Compose related API endpoints.
+
+ These endpoints allow for the generation of message suggestions.
+ """
+
+ def compose_message(
+ self,
+ identifier: str,
+ request_body: ComposeMessageRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[ComposeMessageResponse]:
+ """
+ Compose a message.
+
+ Args:
+ identifier: The identifier of the grant to generate a message suggestion for.
+ request_body: The prompt that smart compose will use to generate a message suggestion.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The generated message.
+ """
+ res, headers = self._http_client._execute(
+ method="POST",
+ path=f"/v3/grants/{identifier}/messages/smart-compose",
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ return Response.from_dict(res, ComposeMessageResponse, headers)
+
+ def compose_message_reply(
+ self,
+ identifier: str,
+ message_id: str,
+ request_body: ComposeMessageRequest,
+ overrides: RequestOverrides = None,
+ ) -> ComposeMessageResponse:
+ """
+ Compose a message reply.
+
+ Args:
+ identifier: The identifier of the grant to generate a message suggestion for.
+ message_id: The id of the message to reply to.
+ request_body: The prompt that smart compose will use to generate a message reply suggestion.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The generated message reply.
+ """
+ res, headers = self._http_client._execute(
+ method="POST",
+ path=f"/v3/grants/{identifier}/messages/{message_id}/smart-compose",
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ return Response.from_dict(res, ComposeMessageResponse, headers)
diff --git a/nylas/resources/threads.py b/nylas/resources/threads.py
new file mode 100644
index 00000000..521ef918
--- /dev/null
+++ b/nylas/resources/threads.py
@@ -0,0 +1,112 @@
+import urllib.parse
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.response import ListResponse, Response, DeleteResponse
+from nylas.models.threads import ListThreadsQueryParams, Thread, UpdateThreadRequest
+
+
+class Threads(
+ ListableApiResource,
+ FindableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Threads API
+
+ The threads API allows you to find, update, and delete threads.
+ """
+
+ def list(
+ self,
+ identifier: str,
+ query_params: ListThreadsQueryParams = None,
+ overrides: RequestOverrides = None,
+ ) -> ListResponse[Thread]:
+ """
+ Return all Threads.
+
+ Args:
+ identifier: The identifier of the grant to get threads for.
+ query_params: The query parameters to filter threads by.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ A list of Threads.
+ """
+ return super().list(
+ path=f"/v3/grants/{identifier}/threads",
+ response_type=Thread,
+ query_params=query_params,
+ overrides=overrides,
+ )
+
+ def find(
+ self, identifier: str, thread_id: str, overrides: RequestOverrides = None
+ ) -> Response[Thread]:
+ """
+ Return a Thread.
+
+ Args:
+ identifier: The identifier of the grant to get the thread for.
+ thread_id: The identifier of the thread to get.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The requested Thread.
+ """
+ return super().find(
+ path=f"/v3/grants/{identifier}/threads/{urllib.parse.quote(thread_id, safe='')}",
+ response_type=Thread,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ identifier: str,
+ thread_id: str,
+ request_body: UpdateThreadRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Thread]:
+ """
+ Update a Thread.
+
+ Args:
+ identifier: The identifier of the grant to update the thread for.
+ thread_id: The identifier of the thread to update.
+ request_body: The request body to update the thread with.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The updated Thread.
+ """
+ return super().update(
+ path=f"/v3/grants/{identifier}/threads/{urllib.parse.quote(thread_id, safe='')}",
+ response_type=Thread,
+ request_body=request_body,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, identifier: str, thread_id: str, overrides: RequestOverrides = None
+ ) -> DeleteResponse:
+ """
+ Delete a Thread.
+
+ Args:
+ identifier: The identifier of the grant to delete the thread for.
+ thread_id: The identifier of the thread to delete.
+ overrides: The request overrides to apply to the request.
+
+ Returns:
+ The deletion response.
+ """
+ return super().destroy(
+ path=f"/v3/grants/{identifier}/threads/{urllib.parse.quote(thread_id, safe='')}",
+ overrides=overrides,
+ )
diff --git a/nylas/resources/transactional_send.py b/nylas/resources/transactional_send.py
new file mode 100644
index 00000000..e6eae0e9
--- /dev/null
+++ b/nylas/resources/transactional_send.py
@@ -0,0 +1,73 @@
+import io
+import urllib.parse
+
+from nylas.config import RequestOverrides
+from nylas.models.messages import Message
+from nylas.models.response import Response
+from nylas.models.transactional_send import TransactionalSendMessageRequest
+from nylas.resources.resource import Resource
+from nylas.utils.file_utils import (
+ MAXIMUM_JSON_ATTACHMENT_SIZE,
+ _build_form_request,
+ encode_stream_to_base64,
+)
+
+
+class TransactionalSend(Resource):
+ """
+ Nylas Transactional Send API.
+
+ Send email from a verified domain without a grant context.
+ """
+
+ def send(
+ self,
+ domain_name: str,
+ request_body: TransactionalSendMessageRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Message]:
+ """
+ Send a transactional email from the specified domain.
+
+ Args:
+ domain_name: The domain Nylas sends from (must be verified in the dashboard).
+ request_body: Message fields; use ``from_`` for the sender (maps to JSON ``from``).
+ overrides: Per-request overrides for the HTTP client.
+
+ Returns:
+ The sent message in a ``Response``.
+ """
+ path = (
+ f"/v3/domains/{urllib.parse.quote(domain_name, safe='')}/messages/send"
+ )
+ form_data = None
+ json_body = None
+
+ if "from_" in request_body and "from" not in request_body:
+ request_body["from"] = request_body["from_"]
+ del request_body["from_"]
+
+ attachment_size = sum(
+ attachment.get("size", 0)
+ for attachment in request_body.get("attachments", [])
+ )
+ if attachment_size >= MAXIMUM_JSON_ATTACHMENT_SIZE:
+ form_data = _build_form_request(request_body)
+ else:
+ for attachment in request_body.get("attachments", []):
+ if issubclass(type(attachment["content"]), io.IOBase):
+ attachment["content"] = encode_stream_to_base64(
+ attachment["content"]
+ )
+
+ json_body = request_body
+
+ json_response, headers = self._http_client._execute(
+ method="POST",
+ path=path,
+ request_body=json_body,
+ data=form_data,
+ overrides=overrides,
+ )
+
+ return Response.from_dict(json_response, Message, headers)
diff --git a/nylas/resources/webhooks.py b/nylas/resources/webhooks.py
new file mode 100644
index 00000000..443c159d
--- /dev/null
+++ b/nylas/resources/webhooks.py
@@ -0,0 +1,184 @@
+import urllib.parse
+
+from nylas.config import RequestOverrides
+from nylas.handler.api_resources import (
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+)
+from nylas.models.response import Response, ListResponse
+from nylas.models.webhooks import (
+ Webhook,
+ WebhookWithSecret,
+ WebhookDeleteResponse,
+ WebhookIpAddressesResponse,
+ CreateWebhookRequest,
+ UpdateWebhookRequest,
+)
+
+
+class Webhooks(
+ ListableApiResource,
+ FindableApiResource,
+ CreatableApiResource,
+ UpdatableApiResource,
+ DestroyableApiResource,
+):
+ """
+ Nylas Webhooks API
+
+ The Nylas webhooks API allows you to manage webhook destinations for your Nylas application.
+ """
+
+ def list(self, overrides: RequestOverrides = None) -> ListResponse[Webhook]:
+ """
+ List all webhook destinations
+
+ Args:
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The list of webhook destinations
+ """
+ return super().list(path="/v3/webhooks", response_type=Webhook)
+
+ def find(
+ self, webhook_id: str, overrides: RequestOverrides = None
+ ) -> Response[Webhook]:
+ """
+ Get a webhook destination
+
+ Parameters:
+ webhook_id: The ID of the webhook destination to get
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The webhook destination
+ """
+ return super().find(
+ path=f"/v3/webhooks/{webhook_id}",
+ response_type=Webhook,
+ overrides=overrides,
+ )
+
+ def create(
+ self, request_body: CreateWebhookRequest, overrides: RequestOverrides = None
+ ) -> Response[WebhookWithSecret]:
+ """
+ Create a webhook destination
+
+ Parameters:
+ request_body: The request body to create the webhook destination
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The created webhook destination
+ """
+ return super().create(
+ path="/v3/webhooks",
+ request_body=request_body,
+ response_type=WebhookWithSecret,
+ overrides=overrides,
+ )
+
+ def update(
+ self,
+ webhook_id: str,
+ request_body: UpdateWebhookRequest,
+ overrides: RequestOverrides = None,
+ ) -> Response[Webhook]:
+ """
+ Update a webhook destination
+
+ Parameters:
+ webhook_id: The ID of the webhook destination to update
+ request_body: The request body to update the webhook destination
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The updated webhook destination
+ """
+ return super().update(
+ path=f"/v3/webhooks/{webhook_id}",
+ request_body=request_body,
+ response_type=Webhook,
+ overrides=overrides,
+ )
+
+ def destroy(
+ self, webhook_id: str, overrides: RequestOverrides = None
+ ) -> WebhookDeleteResponse:
+ """
+ Delete a webhook destination
+
+ Parameters:
+ webhook_id: The ID of the webhook destination to delete
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The response from deleting the webhook destination
+ """
+ return super().destroy(
+ path=f"/v3/webhooks/{webhook_id}",
+ response_type=WebhookDeleteResponse,
+ overrides=overrides,
+ )
+
+ def rotate_secret(
+ self, webhook_id: str, overrides: RequestOverrides = None
+ ) -> Response[WebhookWithSecret]:
+ """
+ Update the webhook secret value for a destination
+
+ Parameters:
+ webhook_id: The ID of the webhook destination to update
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The updated webhook destination
+ """
+ res, headers = self._http_client._execute(
+ method="PUT",
+ path=f"/v3/webhooks/{webhook_id}/rotate-secret",
+ request_body={},
+ overrides=overrides,
+ )
+ return Response.from_dict(res, WebhookWithSecret, headers)
+
+ def ip_addresses(
+ self, overrides: RequestOverrides = None
+ ) -> Response[WebhookIpAddressesResponse]:
+ """
+ Get the current list of IP addresses that Nylas sends webhooks from
+
+ Args:
+ overrides: The request overrides to apply to the request
+
+ Returns:
+ The list of IP addresses that Nylas sends webhooks from
+ """
+ res, headers = self._http_client._execute(
+ method="GET", path="/v3/webhooks/ip-addresses", overrides=overrides
+ )
+ return Response.from_dict(res, WebhookIpAddressesResponse, headers)
+
+
+def extract_challenge_parameter(url: str) -> str:
+ """
+ Extract the challenge parameter from a URL
+
+ Parameters:
+ url: The URL sent by Nylas containing the challenge parameter
+
+ Returns:
+ The challenge parameter
+ """
+ url_object = urllib.parse.urlparse(url)
+ query = urllib.parse.parse_qs(url_object.query)
+ challenge_parameter = query.get("challenge")
+ if not challenge_parameter:
+ raise ValueError("Invalid URL or no challenge parameter found.")
+
+ return challenge_parameter[0]
diff --git a/nylas/utils.py b/nylas/utils.py
deleted file mode 100644
index fa74eae5..00000000
--- a/nylas/utils.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from __future__ import division
-from datetime import datetime, timedelta
-
-
-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())
-
-
-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
-
-
-def convert_metadata_pairs_to_array(data):
- """
- Given a dictionary of metadata pairs, convert it to key-value pairs
- in the format the Nylas API expects: "events?metadata_pair=:"
- """
- if not data:
- return data
-
- metadata_pair = []
- for key, value in data.items():
- metadata_pair.append(key + ":" + value)
-
- return metadata_pair
diff --git a/nylas/utils/__init__.py b/nylas/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nylas/utils/file_utils.py b/nylas/utils/file_utils.py
new file mode 100644
index 00000000..ece1e659
--- /dev/null
+++ b/nylas/utils/file_utils.py
@@ -0,0 +1,81 @@
+import base64
+import json
+import mimetypes
+import os
+from pathlib import Path
+from typing import BinaryIO
+
+from requests_toolbelt import MultipartEncoder
+
+from nylas.models.attachments import CreateAttachmentRequest
+
+
+MAXIMUM_JSON_ATTACHMENT_SIZE = 3 * 1024 * 1024
+"""The maximum size of an attachment that can be sent using json."""
+
+
+def attach_file_request_builder(file_path) -> CreateAttachmentRequest:
+ """
+ Build a request to attach a file.
+
+ Attributes:
+ file_path: The path to the file to attach.
+
+ Returns:
+ A properly-formatted request to attach the file.
+ """
+ path = Path(file_path)
+ filename = path.name
+ size = os.path.getsize(file_path)
+ content_type = mimetypes.guess_type(file_path)[0]
+ file_stream = open(file_path, "rb") # pylint: disable=consider-using-with
+
+ return {
+ "filename": filename,
+ "content_type": content_type if content_type else "application/octet-stream",
+ "content": file_stream,
+ "size": size,
+ }
+
+
+def encode_stream_to_base64(binary_stream: BinaryIO) -> str:
+ """
+ Encode the content of a binary stream to a base64 string.
+
+ Attributes:
+ binary_stream: The binary stream to encode.
+
+ Returns:
+ The base64 encoded content of the binary stream.
+ """
+ binary_stream.seek(0)
+ binary_content = binary_stream.read()
+ return base64.b64encode(binary_content).decode("utf-8")
+
+
+def _build_form_request(request_body: dict) -> MultipartEncoder:
+ """
+ Build a form-data request.
+
+ Attributes:
+ request_body: The request body to send.
+
+ Returns:
+ The multipart/form-data request.
+ """
+ attachments = request_body.get("attachments", [])
+ request_body.pop("attachments", None)
+ message_payload = json.dumps(request_body)
+
+ # Create the multipart/form-data encoder
+ fields = {"message": ("", message_payload, "application/json")}
+ for index, attachment in enumerate(attachments):
+ # Use content_id as field name if provided, otherwise fallback to file{index}
+ field_name = attachment.get("content_id", f"file{index}")
+ fields[field_name] = (
+ attachment["filename"],
+ attachment["content"],
+ attachment["content_type"],
+ )
+
+ return MultipartEncoder(fields=fields)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..46395eff
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,57 @@
+[build-system]
+requires = ["setuptools>=69.0.3", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "nylas"
+dynamic = ["version"]
+description = "Python bindings for the Nylas API platform."
+readme = "README.md"
+license = {text = "MIT"}
+authors = [
+ {name = "Nylas Team", email = "support@nylas.com"}
+]
+keywords = ["inbox", "app", "appserver", "email", "nylas", "contacts", "calendar"]
+requires-python = ">=3.8"
+dependencies = [
+ "requests[security]>=2.31.0",
+ "requests-toolbelt>=1.0.0",
+ "dataclasses-json>=0.5.9",
+ "typing_extensions>=4.7.1",
+ "cryptography>=42.0.0",
+]
+
+[project.optional-dependencies]
+test = [
+ "pytest>=7.4.0",
+ "pytest-cov>=4.1.0",
+ "setuptools>=69.0.3",
+]
+docs = [
+ "mkdocs>=1.5.2",
+ "mkdocstrings[python]>=0.22.0",
+ "mkdocs-material>=9.2.6",
+ "mkdocs-gen-files>=0.5.0",
+ "mkdocs-literate-nav>=0.6.0",
+]
+release = [
+ "bumpversion>=0.6.0",
+ "twine>=4.0.2",
+]
+
+[project.urls]
+Homepage = "https://github.com/nylas/nylas-python"
+Repository = "https://github.com/nylas/nylas-python"
+
+[tool.setuptools.dynamic]
+version = {attr = "nylas._client_sdk_version.__VERSION__"}
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["nylas*"]
+
+[tool.pytest.ini_options]
+addopts = "-m 'not e2e'"
+markers = [
+ "e2e: marks tests that call live Nylas APIs",
+]
diff --git a/scripts/generate-docs.py b/scripts/generate-docs.py
new file mode 100644
index 00000000..26a31a30
--- /dev/null
+++ b/scripts/generate-docs.py
@@ -0,0 +1,45 @@
+"""Generate the code reference pages and navigation."""
+
+from pathlib import Path
+import mkdocs_gen_files
+
+# Set files to exclude from the docs
+excluded_files = [
+ "__init__",
+ "_client_sdk_version",
+ "handler/__init__",
+ "handler/api_resources",
+ "handler/http_client",
+ "models/__init__",
+ "resources/__init__",
+ "utils/__init__",
+]
+
+# Prepare Navigation
+nav = mkdocs_gen_files.Nav()
+
+# Traverse through SDK source files to generate markdown docs for them
+for path in sorted(Path("nylas").rglob("*.py")):
+ # Calculate paths
+ module_path = path.relative_to("nylas").with_suffix("")
+ doc_path = path.relative_to("nylas").with_suffix(".md")
+ full_doc_path = Path("reference", doc_path)
+
+ # Skip excluded files
+ if str(module_path) in excluded_files:
+ continue
+
+ # Add file to navigation
+ parts = tuple(module_path.parts)
+ nav[parts] = doc_path.as_posix()
+
+ # Generate markdown docs
+ with mkdocs_gen_files.open(full_doc_path, "w") as fd:
+ ident = ".".join(parts)
+ fd.write(f"::: {ident}")
+
+ mkdocs_gen_files.set_edit_path(full_doc_path, path)
+
+# Write navigation to SUMMARY.md
+with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file:
+ nav_file.writelines(nav.build_literate_nav())
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 37e7f81a..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,2 +0,0 @@
-[tool:pytest]
-timeout = 10
diff --git a/setup.py b/setup.py
index 6beac07f..bbe31737 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,9 @@
+import os
+import shutil
import sys
import re
import subprocess
-from setuptools import setup, find_packages
-from setuptools.command.test import test as TestCommand
+from setuptools import setup, find_packages, Command
VERSION = ""
@@ -11,27 +12,34 @@
r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE
).group(1)
+with open("README.md", "r", encoding="utf-8") as f:
+ README = f.read()
+
RUN_DEPENDENCIES = [
- "requests[security]>=2.4.2",
- "six>=1.4.1",
- "urlobject",
+ "requests[security]>=2.31.0",
+ "requests-toolbelt>=1.0.0",
+ "dataclasses-json>=0.5.9",
+ "typing_extensions>=4.7.1",
+ "cryptography>=42.0.0",
]
-TEST_DEPENDENCIES = [
- "pytest",
- "pytest-cov",
- "pytest-timeout",
- "responses==0.10.5",
- "twine",
- "pytz",
+
+TEST_DEPENDENCIES = ["pytest>=7.4.0", "pytest-cov>=4.1.0", "setuptools>=69.0.3"]
+
+DOCS_DEPENDENCIES = [
+ "mkdocs>=1.5.2",
+ "mkdocstrings[python]>=0.22.0",
+ "mkdocs-material>=9.2.6",
+ "mkdocs-gen-files>=0.5.0",
+ "mkdocs-literate-nav>=0.6.0",
]
-RELEASE_DEPENDENCIES = ["bumpversion>=0.5.0", "twine>=3.4.2"]
+RELEASE_DEPENDENCIES = ["bumpversion>=0.6.0", "twine>=4.0.2"]
-class PyTest(TestCommand):
+
+class PyTest(Command):
user_options = [("pytest-args=", "a", "Arguments to pass to pytest")]
def initialize_options(self):
- TestCommand.initialize_options(self)
# pylint: disable=attribute-defined-outside-init
self.pytest_args = [
"--cov",
@@ -43,12 +51,11 @@ def initialize_options(self):
self.lint = False
def finalize_options(self):
- TestCommand.finalize_options(self)
# pylint: disable=attribute-defined-outside-init
self.test_args = []
self.test_suite = True
- def run_tests(self):
+ def run(self):
# import here, cause outside the eggs aren't loaded
import pytest
@@ -70,6 +77,19 @@ def main():
except FileNotFoundError as e:
print("Error encountered: {}.\n\n".format(e))
sys.exit()
+ elif sys.argv[1] == "build-docs":
+ if not os.path.exists("docs"):
+ os.makedirs("docs")
+ try:
+ # Copy the README and other markdowns to the docs folder
+ shutil.copy("README.md", "docs/index.md")
+ shutil.copy("Contributing.md", "docs/contributing.md")
+ shutil.copy("LICENSE", "docs/license.md")
+
+ subprocess.check_output(["mkdocs", "build"])
+ except FileNotFoundError as e:
+ print("Error encountered: {}.\n\n".format(e))
+ sys.exit()
elif sys.argv[1] == "release":
if len(sys.argv) < 3:
type_ = "patch"
@@ -89,32 +109,26 @@ def main():
setup(
name="nylas",
version=VERSION,
+ python_requires=">=3.8",
packages=find_packages(),
install_requires=RUN_DEPENDENCIES,
dependency_links=[],
- tests_require=TEST_DEPENDENCIES,
- extras_require={"test": TEST_DEPENDENCIES, "release": RELEASE_DEPENDENCIES},
+ extras_require={
+ "test": TEST_DEPENDENCIES,
+ "docs": DOCS_DEPENDENCIES,
+ "release": RELEASE_DEPENDENCIES,
+ },
cmdclass={"test": PyTest},
author="Nylas Team",
author_email="support@nylas.com",
- description="Python bindings for Nylas, the next-generation email platform.",
+ description="Python bindings for the Nylas API platform.",
license="MIT",
keywords="inbox app appserver email nylas contacts calendar",
url="https://github.com/nylas/nylas-python",
long_description_content_type="text/markdown",
- long_description="""
-# Nylas REST API Python bindings
-
-[](https://codecov.io/gh/nylas/nylas-python)
-
-Python bindings for the Nylas REST API. https://www.nylas.com/docs
-
-The Nylas APIs power applications with email, calendar, and contacts CRUD and bi-directional sync from any inbox in the world.
-
-Nylas is compatible with 100% of email service providers, so you only have to integrate once.
-No more headaches building unique integrations against archaic and outdated IMAP and SMTP protocols.""",
+ long_description=README,
)
if __name__ == "__main__":
- sys.exit(main())
+ main()
diff --git a/tests/.gitignore b/tests/.gitignore
deleted file mode 100644
index f9b48b34..00000000
--- a/tests/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-credentials.py
diff --git a/tests/conftest.py b/tests/conftest.py
index 62500efd..54eaeb81 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,1771 +1,211 @@
-import os
-import re
-import json
-import copy
-import cgi
-import random
-import string
-import pytest
-import responses
-from urlobject import URLObject
-from nylas import APIClient
-
-# pylint: disable=redefined-outer-name,too-many-lines
-
-#### HANDLING PAGINATION ####
-# 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.
-#
-# This file contains mocks for several API endpoints, including "list" endpoints
-# like `/messages` and `/events`. The mocks for these list endpoints must be smart
-# enough to check for an `offset` query param, and return an empty list if the
-# client requests more data than the first page. If the mock does not
-# check for this `offset` query param, and returns the same mock data over and over,
-# any SDK method that tries to fetch *all* of a certain type of data
-# (like `client.messages.all()`) will never complete.
-
-
-def generate_id(size=25, chars=string.ascii_letters + string.digits):
- return "".join(random.choice(chars) for _ in range(size))
-
-
-@pytest.fixture
-def message_body():
- return {
- "busy": True,
- "calendar_id": "94rssh7bd3rmsxsp19kiocxze",
- "description": None,
- "id": "cv4ei7syx10uvsxbs21ccsezf",
- "location": "1 Infinite loop, Cupertino",
- "message_id": None,
- "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn",
- "object": "event",
- "owner": None,
- "participants": [],
- "read_only": False,
- "status": "confirmed",
- "title": "The rain song",
- "when": {
- "end_time": 1441056790,
- "object": "timespan",
- "start_time": 1441053190,
- },
- }
-
-
-@pytest.fixture
-def access_token():
- return "l3m0n_w4ter"
-
-
-@pytest.fixture
-def account_id():
- return "4ennivvrcgsqytgybfk912dto"
-
-
-@pytest.fixture
-def api_url():
- return "https://localhost:2222"
-
-
-@pytest.fixture
-def client_id():
- return "fake-client-id"
-
+from unittest.mock import patch, Mock
-@pytest.fixture
-def client_secret():
- return "nyl4n4ut"
-
-
-@pytest.fixture
-def api_client(api_url):
- return APIClient(
- client_id=None, client_secret=None, access_token=None, api_server=api_url
- )
-
-
-@pytest.fixture
-def api_client_with_client_id(access_token, api_url, client_id, client_secret):
- return APIClient(
- client_id=client_id,
- client_secret=client_secret,
- access_token=access_token,
- api_server=api_url,
- )
-
-
-@pytest.fixture
-def mocked_responses():
- rmock = responses.RequestsMock(assert_all_requests_are_fired=False)
- with rmock:
- yield rmock
+import pytest
+import requests
+from nylas.models.response import Response, ListResponse
+from nylas.handler.http_client import HttpClient
-@pytest.fixture
-def mock_save_draft(mocked_responses, api_url):
- save_endpoint = re.compile(api_url + "/drafts")
- response_body = json.dumps(
- {"id": "4dl0ni6vxomazo73r5oydo16k", "version": "4dw0ni6txomazo33r5ozdo16j"}
- )
- mocked_responses.add(
- responses.POST,
- save_endpoint,
- content_type="application/json",
- status=200,
- body=response_body,
- match_querystring=True,
- )
+from nylas import Client
@pytest.fixture
-def mock_account(mocked_responses, api_url, account_id):
- response_body = json.dumps(
- {
- "account_id": account_id,
- "email_address": "ben.bitdiddle1861@gmail.com",
- "id": account_id,
- "name": "Ben Bitdiddle",
- "object": "account",
- "provider": "gmail",
- "organization_unit": "label",
- "billing_state": "paid",
- "linked_at": 1500920299,
- "sync_state": "running",
- }
- )
- mocked_responses.add(
- responses.GET,
- re.compile(api_url + "/account(?!s)/?"),
- content_type="application/json",
- status=200,
- body=response_body,
+def client():
+ return Client(
+ api_key="test-key",
)
@pytest.fixture
-def mock_accounts(mocked_responses, api_url, account_id, client_id):
- accounts = [
- {
- "account_id": account_id,
- "email_address": "ben.bitdiddle1861@gmail.com",
- "id": account_id,
- "name": "Ben Bitdiddle",
- "object": "account",
- "provider": "gmail",
- "organization_unit": "label",
- "billing_state": "paid",
- "linked_at": 1500920299,
- "sync_state": "running",
- }
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(accounts))
-
- url_re = "{base}(/a/{client_id})?/accounts/?".format(
- base=api_url, client_id=client_id
- )
- mocked_responses.add_callback(
- responses.GET,
- re.compile(url_re),
- content_type="application/json",
- callback=list_callback,
+def http_client():
+ return HttpClient(
+ api_server="https://test.nylas.com",
+ api_key="test-key",
+ timeout=30,
)
@pytest.fixture
-def mock_folder_account(mocked_responses, api_url, account_id):
- response_body = json.dumps(
- {
- "email_address": "ben.bitdiddle1861@office365.com",
- "id": account_id,
- "name": "Ben Bitdiddle",
- "account_id": account_id,
- "object": "account",
- "provider": "eas",
- "organization_unit": "folder",
- }
- )
- mocked_responses.add(
- responses.GET,
- api_url + "/account",
- content_type="application/json",
- status=200,
- body=response_body,
- match_querystring=True,
- )
+def patched_version_and_sys():
+ with patch("sys.version_info", (1, 2, 3, "final", 5)), patch(
+ "nylas.handler.http_client.__VERSION__", "2.0.0"
+ ):
+ yield
@pytest.fixture
-def mock_labels(mocked_responses, api_url, account_id):
- labels = [
- {
- "display_name": "Important",
- "id": "anuep8pe5ugmxrucchrzba2o8",
- "name": "important",
- "account_id": account_id,
- "object": "label",
- },
- {
- "display_name": "Trash",
- "id": "f1xgowbgcehk235xiy3c3ek42",
- "name": "trash",
- "account_id": account_id,
- "object": "label",
- },
- {
- "display_name": "Sent Mail",
- "id": "ah14wp5fvypvjjnplh7nxgb4h",
- "name": "sent",
- "account_id": account_id,
- "object": "label",
- },
- {
- "display_name": "All Mail",
- "id": "ah14wp5fvypvjjnplh7nxgb4h",
- "name": "all",
- "account_id": account_id,
- "object": "label",
- },
- {
- "display_name": "Inbox",
- "id": "dc11kl3s9lj4760g6zb36spms",
- "name": "inbox",
- "account_id": account_id,
- "object": "label",
- },
- ]
+def patched_request():
+ mock_response = Mock()
+ mock_response.content = b"mock data"
+ mock_response.json.return_value = {"foo": "bar"}
+ mock_response.status_code = 200
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(labels))
-
- endpoint = re.compile(api_url + "/labels.*")
- mocked_responses.add_callback(
- responses.GET,
- endpoint,
- content_type="application/json",
- callback=list_callback,
- )
+ with patch("requests.request", return_value=mock_response) as mock_request:
+ yield mock_request
@pytest.fixture
-def mock_label(mocked_responses, api_url, account_id):
- response_body = json.dumps(
- {
- "display_name": "Important",
- "id": "anuep8pe5ugmxrucchrzba2o8",
- "name": "important",
- "account_id": account_id,
- "object": "label",
- }
- )
- url = api_url + "/labels/anuep8pe5ugmxrucchrzba2o8"
- mocked_responses.add(
- responses.GET,
- url,
- content_type="application/json",
- status=200,
- body=response_body,
- )
+def mock_session_timeout():
+ with patch("requests.request", side_effect=requests.exceptions.Timeout):
+ yield
@pytest.fixture
-def mock_folder(mocked_responses, api_url, account_id):
- folder = {
- "display_name": "My Folder",
- "id": "anuep8pe5ug3xrupchwzba2o8",
- "name": None,
- "account_id": account_id,
- "object": "folder",
- }
- response_body = json.dumps(folder)
- url = api_url + "/folders/anuep8pe5ug3xrupchwzba2o8"
- mocked_responses.add(
- responses.GET,
- url,
- content_type="application/json",
- status=200,
- body=response_body,
- )
-
- def request_callback(request):
- payload = json.loads(request.body)
- if "display_name" in payload:
- folder.update(payload)
- return (200, {}, json.dumps(folder))
-
- mocked_responses.add_callback(
- responses.PUT, url, content_type="application/json", callback=request_callback
- )
+def http_client_list_response():
+ with patch(
+ "nylas.models.response.ListResponse.from_dict", return_value=ListResponse([], "bar", None, {"X-Test-Header": "test"})
+ ):
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "abc-123",
+ "data": [],
+ }, {"X-Test-Header": "test"})
+ yield mock_http_client
@pytest.fixture
-def mock_messages(mocked_responses, api_url, account_id):
- messages = [
- {
- "id": "1234",
- "to": [{"email": "foo@yahoo.com", "name": "Foo"}],
- "from": [{"email": "bar@gmail.com", "name": "Bar"}],
- "subject": "Test Message",
- "account_id": account_id,
- "object": "message",
- "labels": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": False,
- "unread": True,
- "date": 1265077342,
- },
- {
- "id": "1238",
- "to": [{"email": "foo2@yahoo.com", "name": "Foo Two"}],
- "from": [{"email": "bar2@gmail.com", "name": "Bar Two"}],
- "subject": "Test Message 2",
- "account_id": account_id,
- "object": "message",
- "labels": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": False,
- "unread": True,
- "date": 1265085342,
- },
- {
- "id": "12",
- "to": [{"email": "foo3@yahoo.com", "name": "Foo Three"}],
- "from": [{"email": "bar3@gmail.com", "name": "Bar Three"}],
- "subject": "Test Message 3",
- "account_id": account_id,
- "object": "message",
- "labels": [{"name": "archive", "display_name": "Archive", "id": "gone"}],
- "starred": False,
- "unread": False,
- "date": 1265093842,
+def http_client_response():
+ with patch(
+ "nylas.models.response.Response.from_dict", return_value=Response({}, "bar", {"X-Test-Header": "test"})
+ ):
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "abc-123",
+ "data": {
+ "id": "calendar-123",
+ "grant_id": "grant-123",
+ "name": "Mock Calendar",
+ "read_only": False,
+ "is_owned_by_user": True,
+ "object": "calendar",
+ },
+ }, {"X-Test-Header": "test"})
+ yield mock_http_client
+
+
+@pytest.fixture
+def http_client_delete_response():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "abc-123",
+ }, {"X-Test-Header": "test"})
+ return mock_http_client
+
+
+@pytest.fixture
+def http_client_token_exchange():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "access_token": "nylas_access_token",
+ "expires_in": 3600,
+ "id_token": "jwt_token",
+ "refresh_token": "nylas_refresh_token",
+ "scope": "https://www.googleapis.com/auth/gmail.readonly profile",
+ "token_type": "Bearer",
+ "grant_id": "grant_123",
+ "provider": "google",
+ }, {"X-Test-Header": "test"})
+ return mock_http_client
+
+
+@pytest.fixture
+def http_client_token_info():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "abc-123",
+ "data": {
+ "iss": "https://nylas.com",
+ "aud": "http://localhost:3030",
+ "sub": "Jaf84d88-ยฃ274-46cc-bbc9-aed7dac061c7",
+ "email": "user@example.com",
+ "iat": 1692094848,
+ "exp": 1692095173,
},
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(messages))
-
- endpoint = re.compile(api_url + "/messages")
- mocked_responses.add_callback(
- responses.GET, endpoint, content_type="application/json", callback=list_callback
- )
-
-
-@pytest.fixture
-def mock_message(mocked_responses, api_url, account_id):
- base_msg = {
- "id": "1234",
- "to": [{"email": "foo@yahoo.com", "name": "Foo"}],
- "from": [{"email": "bar@gmail.com", "name": "Bar"}],
- "subject": "Test Message",
- "account_id": account_id,
- "object": "message",
- "labels": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": False,
- "unread": True,
- }
- response_body = json.dumps(base_msg)
-
- def request_callback(request):
- payload = json.loads(request.body)
- if "labels" in payload:
- labels = [
- {"name": "test", "display_name": "test", "id": l}
- for l in payload["labels"]
- ]
- base_msg["labels"] = labels
- return (200, {}, json.dumps(base_msg))
-
- endpoint = re.compile(api_url + "/messages/1234")
- mocked_responses.add(
- responses.GET,
- endpoint,
- content_type="application/json",
- status=200,
- body=response_body,
- )
- mocked_responses.add_callback(
- responses.PUT,
- endpoint,
- content_type="application/json",
- callback=request_callback,
- )
- mocked_responses.add(
- responses.DELETE, endpoint, content_type="application/json", status=200, body=""
- )
-
-
-@pytest.fixture
-def mock_threads(mocked_responses, api_url, account_id):
- threads = [
- {
- "id": "5678",
- "subject": "Test Thread",
- "account_id": account_id,
- "object": "thread",
- "folders": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": True,
- "unread": False,
- "first_message_timestamp": 1451703845,
- "last_message_timestamp": 1483326245,
- "last_message_received_timestamp": 1483326245,
- "last_message_sent_timestamp": 1483232461,
- }
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(threads))
-
- endpoint = re.compile(api_url + "/threads")
- mocked_responses.add_callback(
- responses.GET,
- endpoint,
- content_type="application/json",
- callback=list_callback,
- )
+ }, {"X-Test-Header": "test"})
+ return mock_http_client
@pytest.fixture
-def mock_thread(mocked_responses, api_url, account_id):
- base_thrd = {
- "id": "5678",
- "subject": "Test Thread",
- "account_id": account_id,
- "object": "thread",
- "folders": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": True,
- "unread": False,
- "first_message_timestamp": 1451703845,
- "last_message_timestamp": 1483326245,
- "last_message_received_timestamp": 1483326245,
- "last_message_sent_timestamp": 1483232461,
- }
- response_body = json.dumps(base_thrd)
-
- def request_callback(request):
- payload = json.loads(request.body)
- if "folder" in payload:
- folder = {"name": "test", "display_name": "test", "id": payload["folder"]}
- base_thrd["folders"] = [folder]
- return (200, {}, json.dumps(base_thrd))
-
- endpoint = re.compile(api_url + "/threads/5678")
- mocked_responses.add(
- responses.GET,
- endpoint,
- content_type="application/json",
- status=200,
- body=response_body,
- )
- mocked_responses.add_callback(
- responses.PUT,
- endpoint,
- content_type="application/json",
- callback=request_callback,
- )
-
-
-@pytest.fixture
-def mock_labelled_thread(mocked_responses, api_url, account_id):
- base_thread = {
- "id": "111",
- "subject": "Labelled Thread",
- "account_id": account_id,
- "object": "thread",
- "folders": [{"name": "inbox", "display_name": "Inbox", "id": "abcd"}],
- "starred": True,
- "unread": False,
- "labels": [
+def http_client_free_busy():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5",
+ "data": [
{
- "display_name": "Important",
- "id": "anuep8pe5ugmxrucchrzba2o8",
- "name": "important",
- "account_id": account_id,
- "object": "label",
+ "email": "user1@example.com",
+ "time_slots": [
+ {
+ "start_time": 1690898400,
+ "end_time": 1690902000,
+ "status": "busy",
+ "object": "time_slot",
+ },
+ {
+ "start_time": 1691064000,
+ "end_time": 1691067600,
+ "status": "busy",
+ "object": "time_slot",
+ },
+ ],
+ "object": "free_busy",
},
{
- "display_name": "Existing",
- "id": "dfslhgy3rlijfhlsujnchefs3",
- "name": "existing",
- "account_id": account_id,
- "object": "label",
+ "email": "user2@example.com",
+ "error": "Unable to resolve e-mail address user2@example.com to an Active Directory object.",
+ "object": "error",
},
],
- "first_message_timestamp": 1451703845,
- "last_message_timestamp": 1483326245,
- "last_message_received_timestamp": 1483326245,
- "last_message_sent_timestamp": 1483232461,
- }
- response_body = json.dumps(base_thread)
-
- def request_callback(request):
- payload = json.loads(request.body)
- if "labels" in payload:
- existing_labels = {label["id"]: label for label in base_thread["labels"]}
- new_labels = []
- for label_id in payload["labels"]:
- if label_id in existing_labels:
- new_labels.append(existing_labels[label_id])
- else:
- new_labels.append(
- {
- "name": "updated",
- "display_name": "Updated",
- "id": label_id,
- "account_id": account_id,
- "object": "label",
- }
- )
- copied = copy.copy(base_thread)
- copied["labels"] = new_labels
- return (200, {}, json.dumps(copied))
-
- endpoint = re.compile(api_url + "/threads/111")
- mocked_responses.add(
- responses.GET,
- endpoint,
- content_type="application/json",
- status=200,
- body=response_body,
- )
- mocked_responses.add_callback(
- responses.PUT,
- endpoint,
- content_type="application/json",
- callback=request_callback,
- )
-
-
-@pytest.fixture
-def mock_drafts(mocked_responses, api_url):
- drafts = [
- {
- "bcc": [],
- "body": "Cheers mate!",
- "cc": [],
- "date": 1438684486,
- "events": [],
- "files": [],
- "folder": None,
- "from": [],
- "id": "2h111aefv8pzwzfykrn7hercj",
- "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn",
- "object": "draft",
- "reply_to": [],
- "reply_to_message_id": None,
- "snippet": "",
- "starred": False,
- "subject": "Here's an attachment",
- "thread_id": "clm33kapdxkposgltof845v9s",
- "to": [{"email": "helena@nylas.com", "name": "Helena Handbasket"}],
- "unread": False,
- "version": 0,
- }
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(drafts))
-
- mocked_responses.add_callback(
- responses.GET,
- api_url + "/drafts",
- content_type="application/json",
- callback=list_callback,
- )
-
-
-@pytest.fixture
-def mock_draft_saved_response(mocked_responses, api_url):
- draft_json = {
- "bcc": [],
- "body": "Cheers mate!",
- "cc": [],
- "date": 1438684486,
- "events": [],
- "files": [],
- "folder": None,
- "from": [],
- "id": "2h111aefv8pzwzfykrn7hercj",
- "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn",
- "object": "draft",
- "reply_to": [],
- "reply_to_message_id": None,
- "snippet": "",
- "starred": False,
- "subject": "Here's an attachment",
- "thread_id": "clm33kapdxkposgltof845v9s",
- "to": [{"email": "helena@nylas.com", "name": "Helena Handbasket"}],
- "unread": False,
- "version": 0,
- }
-
- def create_callback(_request):
- return (200, {}, json.dumps(draft_json))
-
- def update_callback(request):
- try:
- payload = json.loads(request.body)
- except ValueError:
- return (200, {}, json.dumps(draft_json))
-
- stripped_payload = {key: value for key, value in payload.items() if value}
- updated_draft_json = copy.copy(draft_json)
- updated_draft_json.update(stripped_payload)
- updated_draft_json["version"] += 1
- return (200, {}, json.dumps(updated_draft_json))
-
- mocked_responses.add_callback(
- responses.POST,
- api_url + "/drafts",
- content_type="application/json",
- callback=create_callback,
- )
-
- mocked_responses.add_callback(
- responses.PUT,
- api_url + "/drafts/2h111aefv8pzwzfykrn7hercj",
- content_type="application/json",
- callback=update_callback,
- )
-
-
-@pytest.fixture
-def mock_draft_deleted_response(mocked_responses, api_url):
- mocked_responses.add(
- responses.DELETE,
- api_url + "/drafts/2h111aefv8pzwzfykrn7hercj",
- content_type="application/json",
- status=200,
- body="",
- )
-
-
-@pytest.fixture
-def mock_draft_sent_response(mocked_responses, api_url):
- body = {
- "bcc": [],
- "body": "",
- "cc": [],
- "date": 1438684486,
- "events": [],
- "files": [],
- "folder": None,
- "from": [{"email": "benb@nylas.com"}],
- "id": "2h111aefv8pzwzfykrn7hercj",
- "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn",
- "object": "draft",
- "reply_to": [],
- "reply_to_message_id": None,
- "snippet": "",
- "starred": False,
- "subject": "Stay polish, stay hungary",
- "thread_id": "clm33kapdxkposgltof845v9s",
- "to": [{"email": "helena@nylas.com", "name": "Helena Handbasket"}],
- "unread": False,
- "version": 0,
- }
-
- values = [(400, {}, "Couldn't send email"), (200, {}, json.dumps(body))]
-
- def callback(request):
- payload = json.loads(request.body)
- assert payload["draft_id"] == "2h111aefv8pzwzfykrn7hercj"
- return values.pop()
-
- mocked_responses.add_callback(
- responses.POST,
- api_url + "/send",
- callback=callback,
- content_type="application/json",
- )
-
-
-@pytest.fixture
-def mock_draft_send_unsaved_response(mocked_responses, api_url):
- def callback(request):
- payload = json.loads(request.body)
- payload["draft_id"] = "2h111aefv8pzwzfykrn7hercj"
- return 200, {}, json.dumps(payload)
-
- mocked_responses.add_callback(
- responses.POST,
- api_url + "/send",
- callback=callback,
- content_type="application/json",
- )
-
-
-@pytest.fixture
-def mock_files(mocked_responses, api_url, account_id):
- files_content = {"3qfe4k3siosfjtjpfdnon8zbn": b"Hello, World!"}
- files_metadata = {
- "3qfe4k3siosfjtjpfdnon8zbn": {
- "id": "3qfe4k3siosfjtjpfdnon8zbn",
- "content_type": "text/plain",
- "filename": "hello.txt",
- "account_id": account_id,
- "object": "file",
- "size": len(files_content["3qfe4k3siosfjtjpfdnon8zbn"]),
- }
- }
- mocked_responses.add(
- responses.GET,
- api_url + "/files",
- body=json.dumps(list(files_metadata.values())),
- )
- for file_id in files_content:
- mocked_responses.add(
- responses.POST,
- "{base}/files/{file_id}".format(base=api_url, file_id=file_id),
- body=json.dumps(files_metadata[file_id]),
- )
- mocked_responses.add(
- responses.GET,
- "{base}/files/{file_id}/download".format(base=api_url, file_id=file_id),
- body=files_content[file_id],
- )
-
- def create_callback(request):
- uploaded_lines = request.body.decode("utf8").splitlines()
- content_disposition = uploaded_lines[1]
- _, params = cgi.parse_header(content_disposition)
- filename = params.get("filename", None)
- content = "".join(uploaded_lines[3:-1])
- size = len(content.encode("utf8"))
-
- body = [
- {
- "id": generate_id(),
- "content_type": "text/plain",
- "filename": filename,
- "account_id": account_id,
- "object": "file",
- "size": size,
- }
- ]
- return (200, {}, json.dumps(body))
-
- mocked_responses.add_callback(
- responses.POST, api_url + "/files", callback=create_callback
- )
-
-
-@pytest.fixture
-def mock_event_create_response(mocked_responses, api_url, message_body):
- def callback(_request):
- try:
- payload = json.loads(_request.body)
- except ValueError:
- return 400, {}, ""
-
- payload["id"] = "cv4ei7syx10uvsxbs21ccsezf"
- return 200, {}, json.dumps(payload)
-
- mocked_responses.add_callback(
- responses.POST, api_url + "/events", callback=callback
- )
-
- put_body = {"title": "loaded from JSON", "ignored": "ignored"}
- mocked_responses.add(
- responses.PUT,
- api_url + "/events/cv4ei7syx10uvsxbs21ccsezf",
- body=json.dumps(put_body),
- )
-
-
-@pytest.fixture
-def mock_event_create_response_with_limits(mocked_responses, api_url, message_body):
- def callback(request):
- url = URLObject(request.url)
- limit = int(url.query_dict.get("limit") or 50)
- body = [message_body for _ in range(0, limit)]
- return 200, {}, json.dumps(body)
-
- mocked_responses.add_callback(responses.GET, api_url + "/events", callback=callback)
-
-
-@pytest.fixture
-def mock_event_create_notify_response(mocked_responses, api_url, message_body):
- mocked_responses.add(
- responses.POST,
- api_url + "/events?notify_participants=true&other_param=1",
- body=json.dumps(message_body),
- )
-
-
-@pytest.fixture
-def mock_send_rsvp(mocked_responses, api_url, message_body):
- mocked_responses.add(
- responses.POST,
- re.compile(api_url + "/send-rsvp"),
- body=json.dumps(message_body),
- )
-
-
-@pytest.fixture
-def mock_components_create_response(mocked_responses, api_url, message_body):
- def callback(_request):
- try:
- payload = json.loads(_request.body)
- except ValueError:
- return 400, {}, ""
-
- payload["id"] = "cv4ei7syx10uvsxbs21ccsezf"
- return 200, {}, json.dumps(payload)
-
- mocked_responses.add_callback(
- responses.POST, re.compile(api_url + "/component/*"), callback=callback
- )
-
- mocked_responses.add(
- responses.PUT,
- re.compile(api_url + "/component/*"),
- body=json.dumps(message_body),
- )
+ }, {"X-Test-Header": "test"})
+ return mock_http_client
@pytest.fixture
-def mock_thread_search_response(mocked_responses, api_url):
- snippet = (
- "Hey Helena, Looking forward to getting together for dinner on Friday. "
- "What can I bring? I have a couple bottles of wine or could put together"
- )
- response_body = json.dumps(
- [
+def http_client_list_scheduled_messages():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5",
+ "data": [
{
- "id": "evh5uy0shhpm5d0le89goor17",
- "object": "thread",
- "account_id": "awa6ltos76vz5hvphkp8k17nt",
- "subject": "Dinner Party on Friday",
- "unread": False,
- "starred": False,
- "last_message_timestamp": 1398229259,
- "last_message_received_timestamp": 1398229259,
- "first_message_timestamp": 1298229259,
- "participants": [
- {"name": "Ben Bitdiddle", "email": "ben.bitdiddle@gmail.com"}
- ],
- "snippet": snippet,
- "folders": [
- {
- "name": "inbox",
- "display_name": "INBOX",
- "id": "f0idlvozkrpj3ihxze7obpivh",
- }
- ],
- "message_ids": [
- "251r594smznew6yhiocht2v29",
- "7upzl8ss738iz8xf48lm84q3e",
- "ah5wuphj3t83j260jqucm9a28",
- ],
- "draft_ids": ["251r594smznew6yhi12312saq"],
- "version": 2,
- }
- ]
- )
-
- mocked_responses.add(
- responses.GET,
- api_url + "/threads/search?q=Helena",
- body=response_body,
- status=200,
- content_type="application/json",
- match_querystring=True,
- )
-
-
-@pytest.fixture
-def mock_message_search_response(mocked_responses, api_url):
- snippet = (
- "Sounds good--that bottle of Pinot should go well with the meal. "
- "I'll also bring a surprise for dessert. :) "
- "Do you have ice cream? Looking fo"
- )
- response_body = json.dumps(
- [
- {
- "id": "84umizq7c4jtrew491brpa6iu",
- "object": "message",
- "account_id": "14e5bn96uizyuhidhcw5rfrb0",
- "thread_id": "5vryyrki4fqt7am31uso27t3f",
- "subject": "Re: Dinner on Friday?",
- "from": [{"name": "Ben Bitdiddle", "email": "ben.bitdiddle@gmail.com"}],
- "to": [{"name": "Bill Rogers", "email": "wbrogers@mit.edu"}],
- "cc": [],
- "bcc": [],
- "reply_to": [],
- "date": 1370084645,
- "unread": True,
- "starred": False,
- "folder": {
- "name": "inbox",
- "display_name": "INBOX",
- "id": "f0idlvozkrpj3ihxze7obpivh",
+ "schedule_id": "8cd56334-6d95-432c-86d1-c5dab0ce98be",
+ "status": {
+ "code": "pending",
+ "description": "schedule send awaiting send at time",
},
- "snippet": snippet,
- "body": "....",
- "files": [],
- "events": [],
},
{
- "id": "84umizq7asdf3aw491brpa6iu",
- "object": "message",
- "account_id": "14e5bakdsfljskidhcw5rfrb0",
- "thread_id": "5vryyralskdjfwlj1uso27t3f",
- "subject": "Re: Dinner on Friday?",
- "from": [{"name": "Ben Bitdiddle", "email": "ben.bitdiddle@gmail.com"}],
- "to": [{"name": "Bill Rogers", "email": "wbrogers@mit.edu"}],
- "cc": [],
- "bcc": [],
- "reply_to": [],
- "date": 1370084645,
- "unread": True,
- "starred": False,
- "folder": {
- "name": "inbox",
- "display_name": "INBOX",
- "id": "f0idlvozkrpj3ihxze7obpivh",
- },
- "snippet": snippet,
- "body": "....",
- "files": [],
- "events": [],
+ "schedule_id": "rb856334-6d95-432c-86d1-c5dab0ce98be",
+ "status": {"code": "success", "description": "schedule send succeeded"},
+ "close_time": 1690579819,
},
- ]
- )
-
- mocked_responses.add(
- responses.GET,
- api_url + "/messages/search?q=Pinot",
- body=response_body,
- status=200,
- content_type="application/json",
- match_querystring=True,
- )
-
-
-@pytest.fixture
-def mock_calendars(mocked_responses, api_url):
- calendars = [
- {
- "id": "8765",
- "events": [
- {
- "title": "Pool party",
- "location": "Local Community Pool",
- "participants": ["Alice", "Bob", "Claire", "Dot"],
- }
- ],
- }
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(calendars))
-
- endpoint = re.compile(api_url + "/calendars")
- mocked_responses.add_callback(
- responses.GET,
- endpoint,
- content_type="application/json",
- callback=list_callback,
- )
-
-
-@pytest.fixture
-def mock_contacts(mocked_responses, account_id, api_url):
- contact1 = {
- "id": "5x6b54whvcz1j22ggiyorhk9v",
- "object": "contact",
- "account_id": account_id,
- "given_name": "Charlie",
- "middle_name": None,
- "surname": "Bucket",
- "birthday": "1964-10-05",
- "suffix": None,
- "nickname": None,
- "company_name": None,
- "job_title": "Student",
- "manager_name": None,
- "office_location": None,
- "notes": None,
- "picture_url": "{base}/contacts/{id}/picture".format(
- base=api_url, id="5x6b54whvcz1j22ggiyorhk9v"
- ),
- "emails": [{"email": "charlie@gmail.com", "type": None}],
- "im_addresses": [],
- "physical_addresses": [],
- "phone_numbers": [],
- "web_pages": [],
- }
- contact2 = {
- "id": "4zqkfw8k1d12h0k784ipeh498",
- "object": "contact",
- "account_id": account_id,
- "given_name": "William",
- "middle_name": "J",
- "surname": "Wonka",
- "birthday": "1955-02-28",
- "suffix": None,
- "nickname": None,
- "company_name": None,
- "job_title": "Chocolate Artist",
- "manager_name": None,
- "office_location": "Willy Wonka Factory",
- "notes": None,
- "picture_url": None,
- "emails": [{"email": "scrumptious@wonka.com", "type": None}],
- "im_addresses": [],
- "physical_addresses": [],
- "phone_numbers": [],
- "web_pages": [{"type": "work", "url": "http://www.wonka.com"}],
- }
- contact3 = {
- "id": "9fn1aoi2i00qv6h1zpag6b26w",
- "object": "contact",
- "account_id": account_id,
- "given_name": "Oompa",
- "middle_name": None,
- "surname": "Loompa",
- "birthday": None,
- "suffix": None,
- "nickname": None,
- "company_name": None,
- "job_title": None,
- "manager_name": None,
- "office_location": "Willy Wonka Factory",
- "notes": None,
- "picture_url": None,
- "emails": [],
- "im_addresses": [],
- "physical_addresses": [],
- "phone_numbers": [],
- "web_pages": [],
- }
- contacts = [contact1, contact2, contact3]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- if offset:
- return (200, {}, json.dumps([]))
- return (200, {}, json.dumps(contacts))
-
- def create_callback(request):
- payload = json.loads(request.body)
- payload["id"] = generate_id()
- return (200, {}, json.dumps(payload))
-
- for contact in contacts:
- mocked_responses.add(
- responses.GET,
- re.compile(api_url + "/contacts/" + contact["id"]),
- content_type="application/json",
- status=200,
- body=json.dumps(contact),
- )
- if contact.get("picture_url"):
- mocked_responses.add(
- responses.GET,
- contact["picture_url"],
- content_type="image/jpeg",
- status=200,
- body=os.urandom(50),
- stream=True,
- )
- else:
- mocked_responses.add(
- responses.GET,
- "{base}/contacts/{id}/picture".format(base=api_url, id=contact["id"]),
- status=404,
- body="",
- )
- mocked_responses.add_callback(
- responses.GET,
- re.compile(api_url + "/contacts"),
- content_type="application/json",
- callback=list_callback,
- )
- mocked_responses.add_callback(
- responses.POST,
- api_url + "/contacts",
- content_type="application/json",
- callback=create_callback,
- )
-
-
-@pytest.fixture
-def mock_contact(mocked_responses, account_id, api_url):
- contact = {
- "id": "9hga75n6mdvq4zgcmhcn7hpys",
- "object": "contact",
- "account_id": account_id,
- "given_name": "Given",
- "middle_name": "Middle",
- "surname": "Sur",
- "birthday": "1964-10-05",
- "suffix": "Jr",
- "nickname": "Testy",
- "company_name": "Test Data Inc",
- "job_title": "QA Tester",
- "manager_name": "George",
- "office_location": "Over the Rainbow",
- "notes": "This is a note",
- "picture_url": "{base}/contacts/{id}/picture".format(
- base=api_url, id="9hga75n6mdvq4zgcmhcn7hpys"
- ),
- "emails": [
- {"type": "first", "email": "one@example.com"},
- {"type": "second", "email": "two@example.com"},
- {"type": "primary", "email": "abc@example.com"},
- {"type": "primary", "email": "xyz@example.com"},
- {"type": None, "email": "unknown@example.com"},
- ],
- "im_addresses": [
- {"type": "aim", "im_address": "SmarterChild"},
- {"type": "gtalk", "im_address": "fake@gmail.com"},
- {"type": "gtalk", "im_address": "fake2@gmail.com"},
- ],
- "physical_addresses": [
- {
- "type": "home",
- "format": "structured",
- "street_address": "123 Awesome Street",
- "postal_code": "99989",
- "state": "CA",
- "country": "America",
- }
- ],
- "phone_numbers": [
- {"type": "home", "number": "555-555-5555"},
- {"type": "mobile", "number": "555-555-5555"},
- {"type": "mobile", "number": "987654321"},
- ],
- "web_pages": [
- {"type": "profile", "url": "http://www.facebook.com/abc"},
- {"type": "profile", "url": "http://www.twitter.com/abc"},
- {"type": None, "url": "http://example.com"},
],
- }
-
- def update_callback(request):
- try:
- payload = json.loads(request.body)
- except ValueError:
- return (200, {}, json.dumps(contact))
-
- stripped_payload = {key: value for key, value in payload.items() if value}
- updated_contact_json = copy.copy(contact)
- updated_contact_json.update(stripped_payload)
- return (200, {}, json.dumps(updated_contact_json))
-
- mocked_responses.add(
- responses.GET,
- "{base}/contacts/{id}".format(base=api_url, id=contact["id"]),
- content_type="application/json",
- status=200,
- body=json.dumps(contact),
- )
- mocked_responses.add(
- responses.GET,
- contact["picture_url"],
- content_type="image/jpeg",
- status=200,
- body=os.urandom(50),
- stream=True,
- )
-
- mocked_responses.add_callback(
- responses.PUT,
- "{base}/contacts/{id}".format(base=api_url, id=contact["id"]),
- content_type="application/json",
- callback=update_callback,
- )
-
-
-@pytest.fixture
-def mock_events(mocked_responses, api_url):
- events = [
- {
- "id": "1234abcd5678",
- "message_id": "evh5uy0shhpm5d0le89goor17",
- "ical_uid": "19960401T080045Z-4000F192713-0052@example.com",
- "title": "Pool party",
- "location": "Local Community Pool",
- "participants": [
- {
- "comment": None,
- "email": "kelly@nylas.com",
- "name": "Kelly Nylanaut",
- "status": "noreply",
- },
- {
- "comment": None,
- "email": "sarah@nylas.com",
- "name": "Sarah Nylanaut",
- "status": "no",
- },
- ],
- "metadata": {},
- },
- {
- "id": "9876543cba",
- "message_id": None,
- "ical_uid": None,
- "title": "Event Without Message",
- "description": "This event does not have a corresponding message ID.",
- "metadata": {},
- },
- {
- "id": "1231241zxc",
- "message_id": None,
- "ical_uid": None,
- "title": "Event With Metadata",
- "description": "This event uses metadata to store custom values.",
- "metadata": {"platform": "python", "event_type": "meeting"},
- },
- ]
-
- def list_callback(request):
- url = URLObject(request.url)
- offset = int(url.query_dict.get("offset") or 0)
- metadata_key = url.query_multi_dict.get("metadata_key")
- metadata_value = url.query_multi_dict.get("metadata_value")
- metadata_pair = url.query_multi_dict.get("metadata_pair")
-
- if offset:
- return (200, {}, json.dumps([]))
- if metadata_key or metadata_value or metadata_pair:
- results = []
- for event in events:
- if (
- metadata_key
- and set(metadata_key) & set(event["metadata"])
- or metadata_value
- and set(metadata_value) & set(event["metadata"].values())
- ):
- results.append(event)
- elif metadata_pair:
- for pair in metadata_pair:
- key_value = pair.split(":")
- if (
- key_value[0] in event["metadata"]
- and event["metadata"][key_value[0]] == key_value[1]
- ):
- results.append(event)
- return (200, {}, json.dumps(results))
- return (200, {}, json.dumps(events))
-
- endpoint = re.compile(api_url + "/events")
- mocked_responses.add_callback(
- responses.GET, endpoint, content_type="application/json", callback=list_callback
- )
-
-
-@pytest.fixture
-def mock_components(mocked_responses, api_url):
- components = [
- {
- "active": True,
- "settings": {},
- "allowed_domains": [],
- "id": "component-id",
- "name": "PyTest Component",
- "public_account_id": "account-id",
- "public_application_id": "application-id",
- "type": "agenda",
- "created_at": "2021-10-22T18:02:10.000Z",
- "updated_at": "2021-10-22T18:02:10.000Z",
- "accessed_at": None,
- "public_token_id": "token-id",
- },
- ]
-
- def list_callback(arg=None):
- return 200, {}, json.dumps(components)
-
- endpoint = re.compile(api_url + "/component/*")
- mocked_responses.add_callback(
- responses.GET, endpoint, content_type="application/json", callback=list_callback
- )
-
-
-@pytest.fixture
-def mock_resources(mocked_responses, api_url):
- resources = [
- {
- "object": "room_resource",
- "email": "training-room-1A@google.com",
- "name": "Google Training Room",
- "building": "San Francisco",
- "capacity": "10",
- "floor_name": "7",
- "floor_number": None,
- },
- {
- "object": "room_resource",
- "email": "training-room@outlook.com",
- "name": "Microsoft Training Room",
- "building": "Seattle",
- "capacity": "5",
- "floor_name": "Office",
- "floor_number": "2",
- },
- ]
-
- endpoint = re.compile(api_url + "/resources")
- mocked_responses.add(
- responses.GET,
- endpoint,
- body=json.dumps(resources),
- status=200,
- content_type="application/json",
- )
+ }, {"X-Test-Header": "test"})
+ return mock_http_client
@pytest.fixture
-def mock_account_management(mocked_responses, api_url, account_id, client_id):
- account = {
- "account_id": account_id,
- "email_address": "ben.bitdiddle1861@gmail.com",
- "id": account_id,
- "name": "Ben Bitdiddle",
- "object": "account",
- "provider": "gmail",
- "organization_unit": "label",
- "billing_state": "paid",
- }
- paid_response = json.dumps(account)
- account["billing_state"] = "cancelled"
- cancelled_response = json.dumps(account)
-
- upgrade_url = "{base}/a/{client_id}/accounts/{id}/upgrade".format(
- base=api_url, id=account_id, client_id=client_id
- )
- downgrade_url = "{base}/a/{client_id}/accounts/{id}/downgrade".format(
- base=api_url, id=account_id, client_id=client_id
- )
- mocked_responses.add(
- responses.POST,
- upgrade_url,
- content_type="application/json",
- status=200,
- body=paid_response,
- )
- mocked_responses.add(
- responses.POST,
- downgrade_url,
- content_type="application/json",
- status=200,
- body=cancelled_response,
- )
-
-
-@pytest.fixture
-def mock_revoke_all_tokens(mocked_responses, api_url, account_id, client_id):
- revoke_all_url = "{base}/a/{client_id}/accounts/{id}/revoke-all".format(
- base=api_url, id=account_id, client_id=client_id
- )
- mocked_responses.add(
- responses.POST,
- revoke_all_url,
- content_type="application/json",
- status=200,
- body=json.dumps({"success": True}),
- )
-
-
-@pytest.fixture
-def mock_ip_addresses(mocked_responses, api_url, client_id):
- ip_addresses_url = "{base}/a/{client_id}/ip_addresses".format(
- base=api_url, client_id=client_id
- )
- mocked_responses.add(
- responses.GET,
- ip_addresses_url,
- content_type="application/json",
- status=200,
- body=json.dumps(
+def http_client_clean_messages():
+ mock_http_client = Mock()
+ mock_http_client._execute.return_value = ({
+ "request_id": "dd3ec9a2-8f15-403d-b269-32b1f1beb9f5",
+ "data": [
{
- "ip_addresses": [
- "39.45.235.23",
- "23.10.341.123",
- "12.56.256.654",
- "67.20.987.231",
+ "body": "Hello, I just sent a message using Nylas!",
+ "from": [
+ {"name": "Daenerys Targaryen", "email": "daenerys.t@example.com"}
],
- "updated_at": 1552072984,
- }
- ),
- )
-
-
-@pytest.fixture
-def mock_token_info(mocked_responses, api_url, account_id, client_id):
- token_info_url = "{base}/a/{client_id}/accounts/{id}/token-info".format(
- base=api_url, id=account_id, client_id=client_id
- )
- mocked_responses.add(
- responses.POST,
- token_info_url,
- content_type="application/json",
- status=200,
- body=json.dumps(
- {
- "created_at": 1563496685,
- "scopes": "calendar,email,contacts",
- "state": "valid",
- "updated_at": 1563496685,
- }
- ),
- )
-
-
-@pytest.fixture
-def mock_free_busy(mocked_responses, api_url):
- free_busy_url = "{base}/calendars/free-busy".format(base=api_url)
-
- def free_busy_callback(request):
- payload = json.loads(request.body)
- email = payload["emails"][0]
- resp_data = [
- {
- "object": "free_busy",
- "email": email,
- "time_slots": [
- {
- "object": "time_slot",
- "status": "busy",
- "start_time": 1409594400,
- "end_time": 1409598000,
- },
- {
- "object": "time_slot",
- "status": "busy",
- "start_time": 1409598000,
- "end_time": 1409599000,
- },
- ],
- }
- ]
- return 200, {}, json.dumps(resp_data)
-
- mocked_responses.add_callback(
- responses.POST,
- free_busy_url,
- content_type="application/json",
- callback=free_busy_callback,
- )
-
-
-@pytest.fixture
-def mock_availability(mocked_responses, api_url):
- availability_url = "{base}/calendars/availability".format(base=api_url)
-
- def availability_callback(request):
- payload = json.loads(request.body)
- resp_data = {
- "object": "availability",
- "time_slots": [
- {
- "object": "time_slot",
- "status": "free",
- "start_time": 1409594400,
- "end_time": 1409598000,
- },
- {
- "object": "time_slot",
- "status": "free",
- "start_time": 1409598000,
- "end_time": 1409599000,
- },
- ],
- }
-
- return 200, {}, json.dumps(resp_data)
-
- mocked_responses.add_callback(
- responses.POST,
- availability_url,
- content_type="application/json",
- callback=availability_callback,
- )
-
- mocked_responses.add_callback(
- responses.POST,
- "{url}/consecutive".format(url=availability_url),
- content_type="application/json",
- callback=availability_callback,
- )
-
-
-@pytest.fixture
-def mock_sentiment_analysis(mocked_responses, api_url, account_id):
- sentiment_url = "{base}/neural/sentiment".format(base=api_url)
-
- def sentiment_callback(request):
- payload = json.loads(request.body)
- if "message_id" in payload:
- response = [
- {
- "account_id": account_id,
- "processed_length": 11,
- "sentiment": "NEUTRAL",
- "sentiment_score": 0.30000001192092896,
- "text": "hello world",
- }
- ]
- else:
- response = {
- "account_id": account_id,
- "processed_length": len(payload["text"]),
- "sentiment": "NEUTRAL",
- "sentiment_score": 0.30000001192092896,
- "text": payload["text"],
- }
-
- return 200, {}, json.dumps(response)
-
- mocked_responses.add_callback(
- responses.PUT,
- sentiment_url,
- content_type="application/json",
- callback=sentiment_callback,
- )
-
-
-@pytest.fixture
-def mock_extract_signature(mocked_responses, api_url, account_id):
- signature_url = "{base}/neural/signature".format(base=api_url)
-
- def signature_callback(request):
- payload = json.loads(request.body)
- response = {
- "account_id": account_id,
- "body": "This is the body