GithubHelp home page GithubHelp logo

nylas-python's Introduction

Aimeos logo

Nylas Python SDK

PyPI - Version codecov

This is the GitHub repository for the Nylas Python SDK. The repo is primarily for anyone who wants to install the SDK from source or make contributions to it.

If you're looking to use Python to access the Nylas Email, Calendar, or Contacts APIs, see our Python SDK Quickstart guide.

The Nylas platform provides REST APIs for Email, Calendar, and Contacts, and the Python SDK is the quickest way to build your integration using Python.

Here are some resources to help you get started:

If you have any questions about the Nylas platform, please reach out to [email protected].

⚙️ Install

The Nylas Python SDK is available via pip:

pip install nylas --pre

To install the SDK from source, clone this repo and run the install script:

git clone https://github.com/nylas/nylas-python.git && cd nylas-python
python setup.py install

⚡️ Usage

Before you use the Nylas Python SDK, you must first create a Nylas account. Then, follow our API v3 Quickstart guide to set up your first app and get your API keys.

For code samples and example applications, take a look at our Python repos in the Nylas Samples collection.

🚀 Make your first request

After you've installed and set up the Nylas Python SDK, you can make your first API request. To do so, use the Client class from the nylas package.

The SDK is organized into different resources, each of which has methods to make requests to the Nylas API. Each resource is available through the Client object that you configured with your API key. For example, you can use this code to get a list of Calendars:

from nylas import Client

nylas = Client(
    api_key="API_KEY",
)

calendars, request_id, next_cursor = nylas.calendars.list("GRANT_ID")

event, request_id = nylas.events.create(
    identifier="GRANT_ID",
    request_body={
        "title": "test title",
        "description": "test description",
        "when": {
            "start_time": start_unix_timestamp,
            "end_time": end_unix_timestamp,
        }
    },
    query_params={"calendar_id": "primary", "notify_participants": True},
    )
)

event, request_id = nylas.events.find(
    identifier="GRANT_ID",
    event_id=event.id,
    query_params={
        "calendar_id": "primary",
    },
)

nylas.events.destroy("GRANT_ID", event.id, {"calendar_id": "primary"})

📚 Documentation

This SDK makes heavy use of Python 3 dataclasses to define the REST resources and request/response schemas of the Nylas APIs. The Client object is a wrapper around all of these resources and is used to interact with the corresponding APIs. Basic CRUD operations are handled by the create(), find(), list(), update(), and destroy() methods on each resource. Resources may also have other methods which are all detailed in the reference guide for the Python SDK. In the code reference, start at client, and then resources will give more info on available API call methods. models is the place to find schemas for requests, responses, and all Nylas object types.

While most resources are accessed via the top-level Client object, note that auth contains the sub-resource grants as well as a collection of other auth-related API calls.

You'll want to catch nylas.models.errors.NylasAPIError to handle errors.

Have fun!!

✨ Upgrade from v5.x

See UPGRADE.md for instructions on upgrading from v5.x to v6.x.

💙 Contribute

Please refer to Contributing for information about how to make contributions to this project. We welcome questions, bug reports, and pull requests.

🛠️ Debugging

It can sometimes be helpful to turn on request logging during development. Adding the following snippet to your code that calls the SDK should get you sorted:

import logging
import requests

# Set up logging to print out HTTP request information
logging.basicConfig(level=logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

📝 License

This project is licensed under the terms of the MIT license. Please refer to LICENSE for the full terms.

nylas-python's People

Contributors

amanda103 avatar annielcook avatar atejada avatar benlloydpearson avatar berndverst avatar brettgerry avatar emfree avatar erikkai avatar gerdie avatar grinich avatar hooksie avatar jhatch28 avatar kaizen-cmd avatar khamidou avatar kraju3 avatar max-muoto avatar mgilson avatar mhahnenberg avatar mkgs avatar mmautner avatar mrashed-dev avatar nickbair-nylas avatar paultag avatar pfista avatar relaxedtomato avatar singingwolfboy avatar smrutherford avatar spang avatar thedzy avatar waqasjaved160 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

nylas-python's Issues

Token revoking is not supported by nylas-python

There is no convenience method to revoke a token given an APIClient. Currently, I have to do something like:

client = ApiClient(APP_ID, APP_SECRET, access_token=token)
response = client.session.post(client.api_server + '/oauth/revoke')
# handle response myself to make sure it worked.

Ideally, it seems like there should be some method (revoke_token) on the client that handles this for me? I think it could look something like:

@nylas_excepted
def revoke_token(self):
    """Revoke access token.

    If request fails, a Nylas exception gets raised.  If no exception is raised, the request succeeded.
    """
    _validate(self.session.post(self.api_server + '/oauth/revoke'))

install error

pip install nylas

c/_cffi_backend.c:13:17: error: ffi.h: No such file or directory
In file included from c/_cffi_backend.c:61:
c/malloc_closure.h:81: error: expected specifier-qualifier-list before ‘ffi_closure’
c/malloc_closure.h: In function ‘more_core’:
c/malloc_closure.h:117: warning: division by zero
c/malloc_closure.h:149: error: ‘union mmaped_block’ has no member named ‘next’
c/malloc_closure.h: At top level:
c/malloc_closure.h:158: error: expected ‘)’ before ‘*’ token
c/malloc_closure.h:166: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘*’ token
c/_cffi_backend.c:247: error: expected specifier-qualifier-list before ‘ffi_cif’
c/_cffi_backend.c: In function ‘cdataowninggc_dealloc’:
c/_cffi_backend.c:1600: error: ‘ffi_closure’ undeclared (first use in this function)
c/_cffi_backend.c:1600: error: (Each undeclared identifier is reported only once
c/_cffi_backend.c:1600: error: for each function it appears in.)
c/_cffi_backend.c:1600: error: ‘closure’ undeclared (first use in this function)
c/_cffi_backend.c:1600: error: expected expression before ‘)’ token
c/_cffi_backend.c:1603: warning: implicit declaration of function ‘cffi_closure_free’
c/_cffi_backend.c: In function ‘cdataowninggc_traverse’:
c/_cffi_backend.c:1620: error: ‘ffi_closure’ undeclared (first use in this function)
c/_cffi_backend.c:1620: error: ‘closure’ undeclared (first use in this function)
c/_cffi_backend.c:1620: error: expected expression before ‘)’ token
c/_cffi_backend.c: In function ‘cdataowninggc_clear’:
c/_cffi_backend.c:1640: error: ‘ffi_closure’ undeclared (first use in this function)
c/_cffi_backend.c:1640: error: ‘closure’ undeclared (first use in this function)
c/_cffi_backend.c:1640: error: expected expression before ‘)’ token
c/_cffi_backend.c: In function ‘cdataowninggc_repr’:
c/_cffi_backend.c:1801: error: ‘ffi_closure’ undeclared (first use in this function)
c/_cffi_backend.c:1801: error: expected expression before ‘)’ token
c/_cffi_backend.c: At top level:
c/_cffi_backend.c:2377: error: expected declaration specifiers or ‘...’ before ‘ffi_abi’
c/_cffi_backend.c: In function ‘cdata_call’:
c/_cffi_backend.c:2506: error: ‘ffi_abi’ undeclared (first use in this function)
c/_cffi_backend.c:2506: error: expected ‘;’ before ‘fabi’
c/_cffi_backend.c:2548: error: ‘fabi’ undeclared (first use in this function)
c/_cffi_backend.c:2552: error: too many arguments to function ‘fb_prepare_cif’
c/_cffi_backend.c:2557: error: ‘cif_description_t’ has no member named ‘exchange_size’
c/_cffi_backend.c:2567: error: ‘cif_description_t’ has no member named ‘exchange_offset_arg’
c/_cffi_backend.c:2597: error: ‘cif_description_t’ has no member named ‘exchange_offset_arg’
c/_cffi_backend.c:2602: warning: implicit declaration of function ‘ffi_call’
c/_cffi_backend.c:2602: error: ‘cif_description_t’ has no member named ‘cif’
c/_cffi_backend.c: In function ‘new_primitive_type’:
c/_cffi_backend.c:3699: error: ‘ffi_type’ undeclared (first use in this function)
c/_cffi_backend.c:3699: error: ‘ffitype’ undeclared (first use in this function)
c/_cffi_backend.c:3717: error: ‘ffi_type_sint8’ undeclared (first use in this function)
c/_cffi_backend.c:3718: error: ‘ffi_type_sint16’ undeclared (first use in this function)
c/_cffi_backend.c:3719: error: ‘ffi_type_sint32’ undeclared (first use in this function)
c/_cffi_backend.c:3720: error: ‘ffi_type_sint64’ undeclared (first use in this function)
c/_cffi_backend.c:3726: error: ‘ffi_type_float’ undeclared (first use in this function)
c/_cffi_backend.c:3728: error: ‘ffi_type_double’ undeclared (first use in this function)
c/_cffi_backend.c:3730: error: ‘ffi_type_longdouble’ undeclared (first use in this function)
c/_cffi_backend.c:3736: error: ‘ffi_type_uint8’ undeclared (first use in this function)
c/_cffi_backend.c:3737: error: ‘ffi_type_uint16’ undeclared (first use in this function)
c/_cffi_backend.c:3738: error: ‘ffi_type_uint32’ undeclared (first use in this function)
c/_cffi_backend.c:3739: error: ‘ffi_type_uint64’ undeclared (first use in this function)
c/_cffi_backend.c: At top level:
c/_cffi_backend.c:4375: error: expected specifier-qualifier-list before ‘ffi_type’
c/_cffi_backend.c:4394: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘*’ token
c/_cffi_backend.c: In function ‘fb_build’:
c/_cffi_backend.c:4534: error: ‘struct funcbuilder_s’ has no member named ‘atypes’
c/_cffi_backend.c:4534: error: ‘ffi_type’ undeclared (first use in this function)
c/_cffi_backend.c:4534: error: expected expression before ‘)’ token
c/_cffi_backend.c:4535: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4538: error: ‘struct funcbuilder_s’ has no member named ‘rtype’
c/_cffi_backend.c:4538: warning: implicit declaration of function ‘fb_fill_type’
c/_cffi_backend.c:4546: error: ‘cif_description_t’ has no member named ‘exchange_offset_arg’
c/_cffi_backend.c:4549: error: ‘struct funcbuilder_s’ has no member named ‘rtype’
c/_cffi_backend.c:4550: error: ‘ffi_arg’ undeclared (first use in this function)
c/_cffi_backend.c:4560: error: ‘atype’ undeclared (first use in this function)
c/_cffi_backend.c:4573: error: ‘struct funcbuilder_s’ has no member named ‘atypes’
c/_cffi_backend.c:4574: error: ‘struct funcbuilder_s’ has no member named ‘atypes’
c/_cffi_backend.c:4577: error: ‘cif_description_t’ has no member named ‘exchange_offset_arg’
c/_cffi_backend.c:4584: error: ‘cif_description_t’ has no member named ‘exchange_size’
c/_cffi_backend.c: In function ‘fb_build_name’:
c/_cffi_backend.c:4606: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4615: error: ‘struct funcbuilder_s’ has no member named ‘fct’
c/_cffi_backend.c:4617: error: ‘struct funcbuilder_s’ has no member named ‘fct’
c/_cffi_backend.c: In function ‘fb_prepare_ctype’:
c/_cffi_backend.c:4658: error: ‘struct funcbuilder_s’ has no member named ‘fct’
c/_cffi_backend.c:4668: error: ‘struct funcbuilder_s’ has no member named ‘fct’
c/_cffi_backend.c: At top level:
c/_cffi_backend.c:4688: error: expected declaration specifiers or ‘...’ before ‘ffi_abi’
c/_cffi_backend.c: In function ‘fb_prepare_cif’:
c/_cffi_backend.c:4715: warning: implicit declaration of function ‘ffi_prep_cif’
c/_cffi_backend.c:4715: error: ‘cif_description_t’ has no member named ‘cif’
c/_cffi_backend.c:4715: error: ‘fabi’ undeclared (first use in this function)
c/_cffi_backend.c:4715: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4716: error: ‘struct funcbuilder_s’ has no member named ‘rtype’
c/_cffi_backend.c:4716: error: ‘struct funcbuilder_s’ has no member named ‘atypes’
c/_cffi_backend.c:4716: error: ‘FFI_OK’ undeclared (first use in this function)
c/_cffi_backend.c: In function ‘new_function_type’:
c/_cffi_backend.c:4760: error: too many arguments to function ‘fb_prepare_cif’
c/_cffi_backend.c:4774: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4784: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4794: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4797: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4798: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c:4800: error: ‘struct funcbuilder_s’ has no member named ‘nargs’
c/_cffi_backend.c: In function ‘b_new_function_type’:
c/_cffi_backend.c:4811: error: ‘FFI_DEFAULT_ABI’ undeclared (first use in this function)
c/_cffi_backend.c: In function ‘convert_from_object_fficallback’:
c/_cffi_backend.c:4830: error: ‘ffi_arg’ undeclared (first use in this function)
c/_cffi_backend.c: At top level:
c/_cffi_backend.c:4905: error: expected ‘)’ before ‘*’ token
c/_cffi_backend.c: In function ‘b_callback’:
c/_cffi_backend.c:5012: error: ‘ffi_closure’ undeclared (first use in this function)
c/_cffi_backend.c:5012: error: ‘closure’ undeclared (first use in this function)
c/_cffi_backend.c:5039: error: ‘ffi_arg’ undeclared (first use in this function)
c/_cffi_backend.c:5057: warning: implicit declaration of function ‘cffi_closure_alloc’
c/_cffi_backend.c:5075: warning: implicit declaration of function ‘ffi_prep_closure’
c/_cffi_backend.c:5075: error: ‘cif_description_t’ has no member named ‘cif’
c/_cffi_backend.c:5076: error: ‘invoke_callback’ undeclared (first use in this function)
c/_cffi_backend.c:5076: error: ‘FFI_OK’ undeclared (first use in this function)
In file included from c/cffi1_module.c:3,
                 from c/_cffi_backend.c:6219:
c/realize_c_type.c: In function ‘realize_c_type_or_func’:
c/realize_c_type.c:571: error: ‘FFI_DEFAULT_ABI’ undeclared (first use in this function)
c/_cffi_backend.c: In function ‘init_cffi_backend’:
c/_cffi_backend.c:6305: error: ‘FFI_DEFAULT_ABI’ undeclared (first use in this function)
error: Setup script exited with error: command 'gcc' failed with exit status 1

APIClient.account does not contain billing_state

When creating an APIClient with an access token, the account parameter does not contain the billing_state.

If i use the client.accounts.all() and find the account in question, it does have the billing_state. It would be nice to have the client.account rather than search through all client.accounts.all() and find the billing_state of a user.

[IGNORE/TEST] Enhancement integration test

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

Describe the solution you'd like
A clear and concise description of what you want to happen.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Consistent error message for APIClient.api_server

When you instantiate an APIClient with a custom api_server argument, the constructor checks if the api_server contains the string ://. However, if it doesn't, the error message states "When overriding the Nylas API server address, you must include https://".

This error message is inconsistent -- it's possible to create an APIClient with an api_server value of http://example.com, even though that uses insecure HTTP instead of secure HTTPS. The check should be made consistent with the error message, which could mean either changing the check or changing the message.

Need client id and client secret in make_google_blueprint

google_bp = make_google_blueprint(

I believe that client id and client secret need to be passed into make_google_blueprint, otherwise the client id will not be passed to google and an error occurs. Call should be

google_bp = make_google_blueprint(
    client_id= app.config["GOOGLE_OATH_CLIENT_ID"],
    client_secret= app.config["GOOGLE_OATH_CLIENT_SECRET"],
    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.
)

Bug: Draft Tracking Content Not Passed To Send() - Python

The tracking and version objects are not being added to the new draft data when IDs are not set.

Seems like there was a similar issue with Nylas-NodeJS that had been resolved a month ago (link to the resolved issue here - https://githubmemory.com/repo/nylas/nylas-nodejs/issues/231)

Similar to the NodeJS issue above, I believe this is the line that needs to be updated for Python - https://github.com/nylas/nylas-python/blob/main/nylas/client/restful_models.py#L483

Remove bumpversion as a runtime dependency?

While installing nylas, I realized it also pulled the following:

bumpversion = ">=0.5.0"
six = ">=1.4.1"
urlobject = "*"

And indeed it's in the setup.py's install_requires:

"bumpversion>=0.5.0",

This was surprising to me, as this dependency is usually only for cutting new releases, but not actually required at runtime. Indeed, poking around a bit the only usage I see is:

os.system("bumpversion --current-version {} {}".format(VERSION, type_))

Could be worth using extras_require here, so maintainers can still install those dependencies easily:

extras_require

A dictionary mapping names of “extras” (optional features of your project) to strings or lists of strings specifying what other distributions must be installed to support those features. See the section below on Declaring Dependencies for details and examples of the format of this argument.

(See https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords)

Delete message or draft

Following the example to define a namespace:

inbox = APIClient(APP_ID, APP_SECRET, token)

# Get the first namespace
namespace = inbox.namespaces.first()

I was not able to delete a message or draft by its id(error info attached)

namespace.messages.delete('the message id I want to delete')

image

namespace.drafts.delete('the draft id I want to delete')

image

No example of filtering threads by "in" keyword

"in" is a Pytthon global (e.g. if 5 in range(10): print 'hello') as well as a filter criteria for threads to filter by label or folder.

So when I try to retrieve all threads in the "Inbox" folder using this python client library, I encounter a SyntaxError, for example:

>> client.threads.where(in='Inbox').all()
  File "<ipython-input-110-8840a3c28956>", line 1
    client.threads.where(in='Inbox').all()
                          ^
SyntaxError: invalid syntax

How do I perform this filter? I can make a README pull request change if you guys let me know how to do it! :)

Nylas Python doc

On your website nylas.com, the python doc says, to fetch the threads,
we have to initialize the accounts and look for threads on it.
Like this:

account = client.accounts

# Fetch the first thread
thread = account.threads.first()

# Fetch a specific thread
thread = account.threads.find('ac123acd123ef123')

However, I believe the right way to look for a thread does not have to initialize 'accounts'.
Like this:

thread = client.threads.first()

thread = client.threads.find('blahblabblahbahb')

for thread in client.threads.items():
    #do whatever you want

Right?

Otherwise, it gives me an error.

AttributeError: 'RestfulModelCollection' object has no attribute 'threads'

Thanks for the useful API!

send_authorization errors aren't caught

Describe the bug
A clear and concise description of what the bug is.

When calling send_authorization, if the request fails, it will raise a JSONDecodeException.

To Reproduce
Some steps involved to reproduce the bug and any code samples you can share.

// Helps us with reproducing the error :)

Call send_authorization with an invalid, expired, or already used code.

Expected behavior
A clear and concise description of what you expected to happen.

Raise some sort of exception that contains the error message and http status.

SDK Version:
Providing the SDK version can help with the reproduction of the issue and to know if a change could have broken something.

5.5.0

Additional context
Add any other context about the problem here.

        headers = {
            "Content-type": "application/x-www-form-urlencoded",
            "Accept": "text/plain",
        }

This should probably change to somehow allow users to set their own headers. Also, I'm not sure if these are reasonable defaults, what about application/json for both?

Expose full response dict when calling POST `/oauth/token`

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

I don't ask for a user email on my end, so I need to get it from that response.

Describe the solution you'd like
A clear and concise description of what you want to happen.

Just another method that simply returns the response instead of the token.

def trade_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 resp

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

I'm making the call myself right now.

Additional context
Add any other context or screenshots about the feature request here.

Can not recreate calendar with same name after deletion

I'm trying to recreate a calendar with the same name after I delete a previous one but get errors(500,ServerError)
Here's the test code:

inbox = APIClient(APP_ID, APP_SECRET, TOKEN)
namespace = inbox.namespaces.first()
#Create a calendar
cal = namespace.calendars.create()
cal.name = 'testcal6'
cal.save()
print('Calendar testcal6 is created')
calid = cal['id']
#Get calendar id
print(calid)
#Delete calendar
namespace.calendars.delete(calid)
print('Calendar testcal6 is deleted')
#Try to recreate a calendar with same name
print('recreating')
cal = namespace.calendars.create()
cal.name = 'testcal6'
cal.save()

And here's the error info:
image

Issue on Fetching Drafts

On Fetching drafts from Gmail returns wrong results. I Deleted all drafts from my Gmail but when i fetch result from Nylas draft end point, it still returns drafts...
I also tried with curl but same result.

Credentials naming should be consistent

Most of the SDK refers to the Nylas OAuth credentials as "app ID" and "app secret". However, we apparently want to start shifting over to the more standard names: "client ID" and "client secret". Whichever we pick, we should be consistent.

Current usage of "app ID" and "app secret":

  • Constructor arguments for APIClient.
    client = APIClient(app_id="foo", app_secret="bar")
  • Properties of APIClient.
    client.app_id == "foo"; client.app_secret == "bar"
  • Environment variables for autoloading credentials: NYLAS_APP_ID and NYLAS_APP_SECRET
  • pytest fixture in conftest.py: app_id
  • Existing Nylas developer console refers to the credentials as "APP ID" and "APP SECRET"
  • The Nylas API error message refers to "App ID" when this value is missing: mywebsite.com/login/nylas/authorized?reason=Sorry%2C+you+must+provide+a+valid+`client_id`.+The+`client_id`+request+parameter+should+be+the+App+ID+of+your+application+in+the+Nylas+Developer+Portal.&error=access_denied

Current usage of "client ID" and "client secret":

draft.send() does not return thread or message IDs

In v1.1.0, draft.send() does not return the thread or message IDs of the newly created thread (or existing thread, in the case where a send is a reply).

This is highly desirable for recording sends.

Current workaround is to call draft.save() before calling draft.send(), but this can create "orphaned" drafts if the draft.send() fails w/ a 4XX or 5XX server error, cluttering up our users' drafts folder in their email accounts.

offset=0 in RestfulModelCollection is unused

Code is here

It seems to me that the only reason to have offset=0 is to prevent a call like

RestfulModelCollection(..., offset=6)

from actually doing something. However, if a user is motivated, they can still set the default offset in the constructor:

RestfulModelCollection(..., filters={'offset': 6})

So it seems to me that the offset=0 keyword parameter is in the code just to be confusing.
Of course, I very well could be missing something... :-)

Filtering on booleans and datetimes fails

When trying to filter on boolean fields, like starred or unread, the SDK converts the boolean object to a string, so True becomes "True" and False becomes "False". Both of these strings are treated as true by the backend. As a result, it's not possible to filter on boolean False for any boolean fields.

>>> client.threads.where(unread=True).all()
# fetches `https://api.nylas.com/threads?unread=True&offset=0&limit=50`
>>> client.threads.where(unread=False).all()
# fetches `https://api.nylas.com/threads?unread=False&offset=0&limit=50`
# same results either way

In addition, in Python it's common to use the datetime module to refer to dates and times. There are several APIs that return datetimes, or accept datetimes as parameters (like starts_before when filtering events). The SDK only accepts timestamps, and does not work with datetime objects at all. In fact, trying to pass a datetime object to the SDK results in an internal exception, like this:

>>> now
datetime.datetime(2017, 8, 7, 16, 56, 36, 56763)
>>> client.events.where(starts_before=now).all()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/singingwolfboy/clones/nylas-python/nylas/client/restful_model_collection.py", line 45, in all
    return self._range(self.filters['offset'], limit)
  File "/Users/singingwolfboy/clones/nylas-python/nylas/client/restful_model_collection.py", line 118, in _range
    to_fetch)
  File "/Users/singingwolfboy/clones/nylas-python/nylas/client/restful_model_collection.py", line 107, in _get_model_collection
    **filters)
  File "/Users/singingwolfboy/clones/nylas-python/nylas/client/client.py", line 76, in caught
    return func(*args, **kwargs)
  File "/Users/singingwolfboy/clones/nylas-python/nylas/client/client.py", line 259, in _get_resources
    url = str(URLObject(url).add_query_params(filters.items()))
  File "/Users/singingwolfboy/.virtualenvs/nylas-python/lib/python3.6/site-packages/urlobject/urlobject.py", line 464, in add_query_params
    return self.with_query(self.query.add_params(*args, **kwargs))
  File "/Users/singingwolfboy/.virtualenvs/nylas-python/lib/python3.6/site-packages/urlobject/query_string.py", line 67, in add_params
    new = new.add_param(name, value)
  File "/Users/singingwolfboy/.virtualenvs/nylas-python/lib/python3.6/site-packages/urlobject/query_string.py", line 58, in add_param
    parameter = qs_encode(name) + '=' + qs_encode(value)
  File "/Users/singingwolfboy/.virtualenvs/nylas-python/lib/python3.6/site-packages/urlobject/query_string.py", line 138, in _qs_encode_py3
    return urlparse.quote_plus(s)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/parse.py", line 791, in quote_plus
    string = quote(string, safe + space, encoding, errors)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/parse.py", line 775, in quote
    return quote_from_bytes(string, safe)
  File "/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/urllib/parse.py", line 800, in quote_from_bytes
    raise TypeError("quote_from_bytes() expected bytes")
TypeError: quote_from_bytes() expected bytes

SSLError: hostname 'api.nylas.com' doesn't match 'api.cac1.prod.nylas.com'

OS: NAME=Ubuntu 14.04.6
Python Version: 2.7.6
Nylas Version: 4.12.0

I'm working on a legacy project and I'm unable to upgrade Python or Ubuntu. When I try and create a calendar I get the following error:

  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/nylas/client/restful_models.py", line 116, in save
    new_obj = self.api._create_resource(self.cls, self.as_json(), **kwargs)
  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/nylas/client/client.py", line 413, in _create_resource
    response = session.post(url, json=converted_data, headers=headers)
  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/requests/sessions.py", line 504, in post
    return self.request('POST', url, data=data, json=json, **kwargs)
  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/requests/sessions.py", line 461, in request
    resp = self.send(prep, **send_kwargs)
  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/requests/sessions.py", line 573, in send
    r = adapter.send(request, **kwargs)
  File "/root/.virtualenvs/parakeet-api/local/lib/python2.7/site-packages/requests/adapters.py", line 431, in send
    raise SSLError(e, request=request)
SSLError: hostname 'api.nylas.com' doesn't match 'api.cac1.prod.nylas.com'

How do I workaround this error?

`from_` field set by attribute on draft ignored when sending

library version: 4.12.1


we are creating and sending drafts per the documentation here: https://developer.nylas.com/docs/api/#post/send and noticed that the from field in the request is not populated in this case (defaults to null). Setting as 'from' via kwargs works fine. This is visible both in the as_json method and the urllib request logs.

not working example:

>>> draft = client.drafts.create()
>>> draft.from_ = [{'email': '[email protected]', 'name': 'Sender (Test)'}]
>>> draft.to = [{'name': 'Recipient', 'email': '[email protected]'}]
>>> draft.subject = 'test'
>>> draft.body = 'testing'
>>> draft.as_json()
{'bcc': None, 'cc': None, 'body': 'testing', 'date': None, 'files': None, 'from': None, 'id': None, 'account_id': None, 'object': None, 'subject': 'test', 'thread_id': None, 'to': [{'name': 'Recipient', 'email': '[email protected]'}], 'unread': None, 'version': None, 'file_ids': [], 'reply_to_message_id': None, 'reply_to': None, 'starred': None, 'snippet': None, 'tracking': None}
>>> draft.send()
...snipped...
send: b'{"bcc": null, "cc": null, "body": "testing", "date": null, "files": null, "from": null, "id": null, "account_id": null, "object": null, "subject": "test", "thread_id": null, "to": [{"name": "Recipient", "email": "[email protected]"}], "unread": null, "version": null, "file_ids": [], "reply_to_message_id": null, "reply_to": null, "starred": null, "snippet": null, "tracking": null}'
...snipped...
{'account_id': 'xxxx', 'bcc': [], 'body': 'testing', 'cc': [], 'date': 123123123, 'events': [], 'files': [], 'from': [{'email': '[email protected]', 'name': 'name attached to account'}], 'id': 'xxxx', 'labels': [], 'object': 'message', 'reply_to': [], 'snippet': 'testing', 'starred': False, 'subject': 'test', 'thread_id': 'xxxx', 'to': [{'email': '[email protected]', 'name': 'Recipient'}], 'unread': False}

working example:

>>> draft = client.drafts.create(**{'from': [{'email': '[email protected]', 'name': 'Sender (test)'}], 'to': [{'email': '[email protected]'}], 'subject': 'test', 'body': 'test2'})
>>> draft.as_json()
{'bcc': None, 'cc': None, 'body': 'test2', 'date': None, 'files': None, 'from': [{'email': '[email protected]', 'name': 'Sender (test)'}], 'id': None, 'account_id': None, 'object': None, 'subject': 'test', 'thread_id': None, 'to': [{'email': '[email protected]'}], 'unread': None, 'version': None, 'file_ids': [], 'reply_to_message_id': None, 'reply_to': None, 'starred': None, 'snippet': None, 'tracking': None}
>>> draft.send()
 ...snipped request log...
send: b'{"bcc": null, "cc": null, "body": "test2", "date": null, "files": null, "from": [{"email": "[email protected]", "name": "Sender (test)"}], "id": null, "account_id": null, "object": null, "subject": "test", "thread_id": null, "to": [{"email": "[email protected]"}], "unread": null, "version": null, "file_ids": [], "reply_to_message_id": null, "reply_to": null, "starred": null, "snippet": null, "tracking": null}'
 ...snipped request log...
{'account_id': 'xxxx', 'bcc': [], 'body': 'test2', 'cc': [], 'date': 123123123, 'events': [], 'files': [], 'from': [{'email': '[email protected]', 'name': 'Sender (test)'}], 'id': 'xxxx', 'labels': [], 'object': 'message', 'reply_to': [], 'snippet': 'test2', 'starred': False, 'subject': 'test', 'thread_id': 'xxxx', 'to': [{'email': '[email protected]', 'name': ''}], 'unread': False}

Cannot use timezone aware datetimes in messages API

Hi,
I noticed that I cannot pass a timezone aware datetime object to the messages.where() API without getting a TypeError.

Steps to reproduce

>>> import datetime
>>> from nylas.client import APIClient
>>> client = APIClient(MY_CLIENT_ID, MY_SECRET, access_token=MY_ACCESS_TOKEN)
>>> after = datetime.datetime.fromisoformat('2020-03-31T06:50:23.756830+00:00')
>>> after
datetime.datetime(2020, 3, 31, 6, 50, 23, 756830, tzinfo=datetime.timezone.utc)
>>> for msg in client.messages.where(received_after=after):
...     print(msg.id)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/.../nylas/client/restful_model_collection.py", line 27, in values
    models = self._get_model_collection(offset, CHUNK_SIZE)
  File "/.../nylas/client/restful_model_collection.py", line 114, in _get_model_collection
    return self.api._get_resources(self.model_class, **filters)
  File "/.../nylas/client/client.py", line 320, in _get_resources
    filters, cls.datetime_filter_attrs
  File "/.../nylas/utils.py", line 28, in convert_datetimes_to_timestamps
    new_data[new_key] = timestamp_from_dt(value)
  File "/.../nylas/utils.py", line 10, in timestamp_from_dt
    delta = dt - epoch
TypeError: can't subtract offset-naive and offset-aware datetimes

Expected behaviour

I would assume that the API is either restricted to expecting UNIX timestamps only, or that it handles both aware and naive datetimes correctly. The current behaviour feels inconsistent to me:

  • client.messages.where(received_after=1585637423): works ✔️
  • client.messages.where(received_after=datetime.datetime(2020, 3, 31, 6, 50, 23)): works ✔️
  • client.messages.where(received_after=datetime.datetime(2020, 3, 31, 6, 50, 23, tzinfo=datetime.timezone.utc)): doesn't work ❌

Webhook support

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

I can't use the python sdk to register my webhooks, and the authentication scheme wasn't obvious, it's Basic <client_secret>: where "<client_secret>:" is b64 encoded). I tried it without the ":" at first, then I figured out I needed to b64 a ":" character with my secret as well.

Describe the solution you'd like
A clear and concise description of what you want to happen.

One method to register the webhook and either 1. constants of the webhook types or 2. an enum with the webhook types.

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Currently making the request myself

    headers = {
        "Accept": "application/json",
        "Authorization": f"Basic {get_encoded_nylas_secret()}",
        "Content-Type": "application/json",
    }

    data = {
        "callback_url": url,
        "triggers": ["message.opened", "message.link_clicked", "thread.replied"],
        "state": "active",
    }

    return requests.post(
        f"https://api.nylas.com/a/{settings.NYLAS_CLIENT_ID}/webhooks",
        data=json.dumps(data),
        headers=headers,
    )

Additional context
Add any other context or screenshots about the feature request here.

A rough eta of webhook support would be nice, my goal is just to reduce the number of raw calls to nylas in my code, and try to use the sdk wherever possible.

No way to specify Nylas-API-Version (2.1 required for event.ics_uid)

Right now, I'm using this hack to create a nylas.APIClient that sends the header Nylas-API-Version: 2.1:

def nylas_with_v21(token):
    client = nylas.APIClient(None, None, token.value)
    client.session.headers["Nylas-API-Version"] = "2.1"
    return client

This isn't ideal and the version should probably a) be a constructor parameter and b) default to the latest version if none is specified. (Currently if you do not specify a version header explicitly, it assumes the oldest version (?) and I do not receive event.ics_uid.)

It might be that a default version is also specified from the Nylas dashboard, but it'd still be nice to support setting it explicitly in case a user's application needs to use the new version in only a few places for some reason.

Adding the tracking attribute causes Internal Server Error.

This is my code:

from nylas import APIClient

       nylas = APIClient(
           client_id,
           client_secret,
           access_token
       )
       draft = nylas.drafts.create()
       draft.subject = "With Love, from Nylas"
       draft.body = "This email was sent using the Nylas Email API. Visit https://nylas.com for details."
       draft.to = [{'name': 'My Nylas Friend', 'email': '[email protected]'}]
       # draft.tracking = {'links': 'false', 'opens': "true", 'thread_replies': 'false', 'payload': 'python sdk open tracking test'}
       draft.send()

This works perfectly fine and an email is sent.

However, the moment I uncomment the draft.tracking line, I receive the following error:

 File "//nylas/client/restful_models.py", line 465, in send
    msg = self.api._create_resource(Send, data)
  File "//nylas/client/client.py", line 353, in _create_resource
    result = _validate(response).json()
  File "//nylas/client/client.py", line 54, in _validate
    response.raise_for_status()
  File "//requests/models.py", line 940, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 500 Server Error: INTERNAL SERVER ERROR for url: https://api.nylas.com/send/

I'll try sending it via curl later on and report back, but this error from the Python SDK is happening despite this commit: d7a42dd

Handle HTTP success status codes beyond 200

There are many HTTP status codes that indicate different kinds of successful responses. "200 OK" is the most widely used, but many RESTful APIs use "201 Created" in response to creating resources, and "204 No Content" in response to deleting resources.

This project is hardcoded to only accept the 200 status code as success, which means that if the API ever returns a different, non-200 status code to indicate a successful response, the SDK will throw an exception. This project should work with success status codes beyond 200.

When using the requests library, you can use response.ok to determine if the response was successful. You can also use response.raise_for_status() to raise an exception if necessary, rather than trying to determine if the request was successful and raising an exception if it wasn't.

Gmail contact returning empty contacts even on source=adress_book

I am trying to test out the client and have an email with a single user in my address book. It is my work email so I have many emails in the account.

This returns several unknown and empty emails, including what I assume are ones generated from emails.

client = nylas.APIClient(APP_ID, APP_SECRET, AUTH_TOKEN)
for contact in client.contacts:
    print(contact.as_json())

However even when adding source=address_book I get less, but still some empty emails.

client = nylas.APIClient(APP_ID, APP_SECRET, AUTH_TOKEN)
for contact in client.contacts.where(source='address_book'):
    print(contact.as_json())

Am I misusing the source filter? I am currently using nylas==4.2.0

Send a RSVP-ing while creating an event

Hi, I'm am trying to send a RSVP-ing email when creating a event through nylas-python... I'm currently using the code below:

from nylas import APIClient
client = APIClient(APP_ID, APP_SECRET, NYLAS_CODE)
event = client.events.create()
event.title = "My title !"
event.when = {"start_time": start_date, "end_time": stop_date}
event.location = "Paris"
event.participants = [{"name": "my name", 'email': '[email protected]'}]
calendar = filter(lambda cal: not cal.read_only, client.calendars)[0]
event.calendar_id = calendar.id
event.save()

I couldn't find in the doc how to specify that participants should receive an email invitation.
Thanks

The `collection.items` method does not obey the `offset` parameter

From a discussion on the Community Slack channel. This function call:

client.threads.where(in_='inbox', limit=thread_size, offset=mail_offset).items()

Does not behave as expected. items ignores the provided offset and always returns starting with the 0th item. (https://github.com/nylas/nylas-python/blob/master/nylas/client/restful_model_collection.py#L22).

Alternatively, there's a method called all, but confusingly it obeys the preset offset but requires that you pass the limit as a parameter, ignoring the limit set in the where clause. This is also reported as not working as expected.

client.threads.where(in_='inbox', offset=mail_offset).all(limit=thread_size)

Ideally items and allwould be merged together, and would obey the limit and offset provided in a where clause.

Account object doesn't contain organization_unit

Here is the code I'm running

from nylas import APIClient

nylas = APIClient(
    APP_ID,
    APP_SECRET
)

account = nylas.accounts.first()
print(account)

account should contain a value for organization_unit that indicates if the account uses labels or folders, but it is non-existent.

Refactor APIAccount / Account

The difference between an APIAccount and an Account is a little confusing. This should be renamed to something similar to "AuthenticatedAccount" and "AccountManager".

One issues caused by this is that you're not able to filter on client.accounts to call the downgrade method. For example - this doesn't work:

client = APIClient(config.get("nylas", "app_id"), config.get("nylas", "app_secret"))
account = client.accounts.get(account_id)
account.downgrade()

Further, there is a collision on client.accounts - it can refer to two different authentication schemes to Nylas' API. Either that regular api.nylas.com, or the account management scopes api.nylas.com/a/. These should be distinct, something like client.manage_accounts and client.account

Right now, if someone wants to downgrade an account they must have a client with the authenticated account connected.

client = APIClient(config.get("nylas", "app_id"), config.get("nylas", "app_secret"), account["nylas_token"])
account_id_to_downgrade = client.account.id
client.revoke_token()
for account in client.accounts:
    if account.id == account_id_to_downgrade:
        account.downgrade()

Ideally, they'd be able to do something like this:

client = APIClient(config.get("nylas", "app_id"), config.get("nylas", "app_secret"))
account = client.manage_accounts.get(account["nylas_id"])
account.downgrade()

How do you add metadata on a draft message?

Describe the bug
I'm trying to add a metadata attr on the draft but it's not appearing on the created webhook

To Reproduce

draft = self.nylas_client.drafts.create()
...
draft.metadata = {"extra": 1}
draft.send()

SDK Version:
Version: 5.10.0

requests and six dependencies not working in pip

I was just trying to install inbox using pip in a virtualenv. First time I ran pip install inbox, there was an error saying that requests could not be found. After I installed requests, it said the same about six. After that, pip install worked fine. Just an FYI.

File.filename doesn't apply when type is stream:

Setting a filename on an attachment only works when using .data. When .stream is used, it falls back to the name of the file on the uploading system.

        myfile = self.nylas_client.files.create()
        myfile.content_type = 'application/pdf'
        myfile.filename = attachment_name
        with open(attachment_path, 'rb') as f:
            myfile.stream = f
            myfile.save()

        myfile.filename = attachment_name
        # Create a new draft
        draft = self.nylas_client.drafts.create()
        if type(recipients) == str:
            recipients = [recipients]
        draft.to = [{'email': recipient} for recipient in recipients]
        draft.subject = subject
        draft.body = message
        draft.attach(myfile)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.