From c76f66a5ffa6f70159136a2773f870570741df8b Mon Sep 17 00:00:00 2001 From: mikey Date: Tue, 9 Aug 2016 11:36:34 -0700 Subject: [PATCH 001/451] Adds search Summary: Adds search capability for Messages and Threads. Test Plan: Added tests Fixes #15 --- README.md | 12 ++ nylas/client/client.py | 9 +- nylas/client/restful_model_collection.py | 8 ++ tests/test_search.py | 150 +++++++++++++++++++++++ 4 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 tests/test_search.py diff --git a/README.md b/README.md index 09df78c0..06a7b81d 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,18 @@ for thread in client.threads.where(any_email='ben@nylas.com').items(): print thread.subject ``` +### Searching Messages and Threads + +You can perform a full-text search to find Messages and Threads that contain +your search query. The search is proxied to the mail-provider. + +```python +# Find all threads with the word "nylas" +threads = client.threads.search("nylas") + +# Find all messages with the word "nylas" +messages = client.messages.search("nylas") +``` ### Working with Threads and Messages diff --git a/nylas/client/client.py b/nylas/client/client.py index a391a4ea..5cfc9759 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -227,14 +227,15 @@ def _get_http_session(self, api_root): return self.session @nylas_excepted - def _get_resources(self, cls, **filters): + 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 '' if cls.api_root != 'a': - url = "{}/{}".format(self.api_server, cls.collection_name) + url = "{}/{}{}".format(self.api_server, cls.collection_name, postfix) else: - url = "{}/a/{}/{}".format(self.api_server, self.app_id, - cls.collection_name) + url = "{}/a/{}/{}{}".format(self.api_server, self.app_id, + cls.collection_name, postfix) url = url_concat(url, filters) response = self._get_http_session(cls.api_root).get(url) diff --git a/nylas/client/restful_model_collection.py b/nylas/client/restful_model_collection.py index 191dcc25..38308c75 100644 --- a/nylas/client/restful_model_collection.py +++ b/nylas/client/restful_model_collection.py @@ -70,6 +70,14 @@ def create(self, **kwargs): def delete(self, id, data=None, **kwargs): return self.api._delete_resource(self.model_class, id, data=data, **kwargs) + def search(self, q): + from .restful_models import (Message, Thread) + 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: diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 00000000..9c813306 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,150 @@ +import json +import pytest +import responses +from conftest import API_URL +from nylas.client.errors import InvalidRequestError + +@pytest.fixture +def mock_thread_search_response(): + response_body = json.dumps( + [ + { + "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": "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", + "folders": [ + { + "name": "inbox", + "display_name": "INBOX", + "id": "f0idlvozkrpj3ihxze7obpivh" + }, + ], + "message_ids": [ + "251r594smznew6yhiocht2v29", + "7upzl8ss738iz8xf48lm84q3e", + "ah5wuphj3t83j260jqucm9a28" + ], + "draft_ids": [ + "251r594smznew6yhi12312saq" + ], + "version": 2 + } + ]) + + 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(): + 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" + }, + "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", + "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": "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", + "body": "....", + "files": [], + "events": [] + } + ]) + + responses.add(responses.GET, + API_URL + '/messages/search?q=Pinot', + body=response_body, status=200, + content_type='application/json', + match_querystring=True) + + +@responses.activate +def test_search_threads(api_client, mock_thread_search_response): + threads = api_client.threads.search("Helena") + assert len(threads) == 1 + assert "Helena" in threads[0].snippet + +@responses.activate +def test_search_messages(api_client, mock_message_search_response): + messages = api_client.messages.search("Pinot") + assert len(messages) == 2 + assert "Pinot" in messages[0].snippet + assert "Pinot" in messages[1].snippet + +@responses.activate +def test_search_drafts(api_client, mock_message_search_response): + with pytest.raises(Exception): + api_client.drafts.search("Pinot") From 08e46969de2ce2f12e0b38ac2cb0d4782da7436d Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 9 Aug 2016 11:47:06 -0700 Subject: [PATCH 002/451] =?UTF-8?q?Bump=20version:=201.2.2=20=E2=86=92=201?= =?UTF-8?q?.2.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- nylas/_client_sdk_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 35c8efc2..3188362b 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 1.2.2 +current_version = 1.2.3 [bumpversion:file:nylas/_client_sdk_version.py] diff --git a/nylas/_client_sdk_version.py b/nylas/_client_sdk_version.py index c18258b6..02357ebb 100644 --- a/nylas/_client_sdk_version.py +++ b/nylas/_client_sdk_version.py @@ -1 +1 @@ -__VERSION__ = "1.2.2" +__VERSION__ = "1.2.3" From 31c3f81d912765fd31950a9b2ed8c68976fe898a Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 9 Aug 2016 11:50:32 -0700 Subject: [PATCH 003/451] update changelog for 1.2.3 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b53cba4a..37aea251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ nylas-python Changelog ====================== +v1.2.3 +------ + +Release August 9, 2016: +- Adds full-text search support for Messages and Threads + v1.2.2 ------ From 6ca9387ba6054c533f05e669471d7c232aa1978a Mon Sep 17 00:00:00 2001 From: mikey Date: Mon, 22 Aug 2016 09:32:16 -0700 Subject: [PATCH 004/451] Nylas connect example Adds an example flask app showing how to get a google refresh token and then connect that account to Nylas using native auth API. --- .gitignore | 3 + examples/nylas-connect/.gitignore | 3 + examples/nylas-connect/app.py | 187 ++++++++++++++++++ .../nylas-connect/credentials.py.template | 4 + examples/nylas-connect/readme.md | 103 ++++++++++ examples/nylas-connect/requirements.txt | 19 ++ examples/nylas-connect/templates/index.html | 21 ++ 7 files changed, 340 insertions(+) create mode 100644 examples/nylas-connect/.gitignore create mode 100755 examples/nylas-connect/app.py create mode 100644 examples/nylas-connect/credentials.py.template create mode 100644 examples/nylas-connect/readme.md create mode 100644 examples/nylas-connect/requirements.txt create mode 100644 examples/nylas-connect/templates/index.html diff --git a/.gitignore b/.gitignore index f7f6ed15..29bfda89 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,6 @@ include/ lib/ local/ .eggs/ + +pip-selfcheck.json +tests/output diff --git a/examples/nylas-connect/.gitignore b/examples/nylas-connect/.gitignore new file mode 100644 index 00000000..cb272b17 --- /dev/null +++ b/examples/nylas-connect/.gitignore @@ -0,0 +1,3 @@ +credentials.py +env/ +*.pyc diff --git a/examples/nylas-connect/app.py b/examples/nylas-connect/app.py new file mode 100755 index 00000000..e5ab2ba8 --- /dev/null +++ b/examples/nylas-connect/app.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +import json +import sys +import os +import flask +import requests +import urllib +import logging +import subprocess + +from nylas import APIClient + +# Sets the logging format +logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s') +log = logging.getLogger(__name__) +log.setLevel(logging.INFO) + +try: + from credentials import * +except ImportError: + log.error("Couldn't import credentials.py --- you'll need to create it.") + log.error("See credentials.py.template for more details.") + sys.exit(-1) + +app = flask.Flask(__name__) + +# These are the permissions your app will ask the user to approve for access +# https://developers.google.com/identity/protocols/OAuth2WebServer#scope +GOOGLE_SCOPES = ' '.join(['https://mail.google.com/', + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/calendar', + 'https://www.google.com/m8/feeds/']) + +# This is the path in your Google application that users are redirected to after they +# have authenticated with Google, and it must be authorized through Google's +# developer console +REDIRECT_URI = '' # Note: Use your ngrok url here if testing locally +NYLAS_API = 'https://api.nylas.com' +OAUTH_TOKEN_VALIDATION_URL = 'https://www.googleapis.com/oauth2/v2/tokeninfo' + + +@app.route('/') +def index(): + if 'google_credentials' not in flask.session: + return flask.render_template('index.html') + + # The user has authorized with google at this point but we will need to + # connect the account to Nylas + if 'nylas_access_token' not in flask.session: + google_credentials = flask.session['google_credentials'] + google_access_token = google_credentials['access_token'] + email_address = get_email(google_access_token) + google_refresh_token = google_credentials['refresh_token'] + connect_to_nylas(google_refresh_token, email_address) + return flask.redirect(flask.url_for('index')) + + # Google account has been setup, let's use Nylas' python SDK to retrieve an + # email + client = APIClient(NYLAS_CLIENT_ID, NYLAS_CLIENT_SECRET, + flask.session['nylas_access_token']) + + # Display the latest email message! + return client.threads.first().messages.first().body + + +# This is the url Google will call once a user has approved access to their +# account +@app.route('/oauth2callback') +def oauth2callback(): + if 'code' not in flask.request.args: + params = {'response_type': 'code', + 'access_type': 'offline', + 'client_id': GOOGLE_CLIENT_ID, + 'redirect_uri': REDIRECT_URI, + 'scope': GOOGLE_SCOPES, + # Note: this is only for testing to ensure a refresh token is + # passed everytime, but requires the user to approve offline + # access every time. You should remove this if you don't want + # your user to have to approve access each time they connect + 'prompt': 'consent', + } + url_params = urllib.urlencode(params) + auth_uri = 'https://accounts.google.com/o/oauth2/v2/auth?{}'.format(url_params) + return flask.redirect(auth_uri) + else: + auth_code = flask.request.args.get('code') + data = {'code': auth_code, + 'client_id': GOOGLE_CLIENT_ID, + 'client_secret': GOOGLE_CLIENT_SECRET, + 'redirect_uri': REDIRECT_URI, + 'grant_type': 'authorization_code'} + r = requests.post('https://www.googleapis.com/oauth2/v4/token', data=data) + # This refresh token will only be returned once unless you prompt the user + # for consent every time, so be sure to remember it! + flask.session['google_credentials'] = r.json() + return flask.redirect(flask.url_for('index')) + + +# Connecting an account is a two step process once you have a refresh token from +# Google. +# First POST to /connect/authorize to get an authorization code from Nylas +# Then post to /connect/token to get an access_token that can be used to access +# account data +def connect_to_nylas(google_refresh_token, email_address): + google_settings = {'google_client_id': GOOGLE_CLIENT_ID, + 'google_client_secret': GOOGLE_CLIENT_SECRET, + 'google_refresh_token': google_refresh_token + } + + data = {'client_id': NYLAS_CLIENT_ID, + 'name': 'Your Name', + 'email_address': email_address, + 'provider': 'gmail', + 'settings': google_settings + } + code = nylas_code(data) + + data = {'client_id': NYLAS_CLIENT_ID, + 'client_secret': NYLAS_CLIENT_SECRET, + 'code': code + } + nylas_access_token = nylas_token(data) + + flask.session['nylas_access_token'] = nylas_access_token + + +# Uses googles tokeninfo endpoint to get an email address from the google +# access_token +def get_email(google_access_token): + r = requests.get(OAUTH_TOKEN_VALIDATION_URL, + params={'access_token': google_access_token, + 'fields': 'email'}) # specify we only want the email + resp = r.json() + log.info(resp) + return resp['email'] + + +def nylas_code(data): + connect_uri = '{}/connect/authorize'.format(NYLAS_API) + resp = requests.post(connect_uri, json=data).json() + log.info(resp) + if 'code' in resp: + return resp['code'] + + raise Exception("Error getting auth code from Nylas", err=resp) + + +def nylas_token(data): + token_uri = '{}/connect/token'.format(NYLAS_API) + resp = requests.post(token_uri, json=data).json() + log.info(resp) + if 'access_token' in resp: + return resp['access_token'] + + raise Exception("Error getting access token from Nylas", err=resp) + + +# Setup ngrok and google developer settings to ensure everything works locally +def initialize(): + # Make sure ngrok is running + try: + resp = requests.get('http://localhost:4040/api/tunnels').json() + except requests.exceptions.ConnectionError: + print "It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'" + sys.exit(-1) + + global REDIRECT_URI + REDIRECT_URI = "{}/oauth2callback".format(resp['tunnels'][0]['public_url']) + print REDIRECT_URI + s = raw_input("Have you added the url above as an authorized callback " + "in Google's Developer console? y/n ") + if s != "y": + print "You need to set that up first!" + print "See https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide for more information" + sys.exit(-1) + + +if __name__ == '__main__': + logging.info("Initializing Application") + import uuid + initialize() + app.secret_key = str(uuid.uuid4()) + app.debug = False + print "Visit http://localhost:1234 in your browser" + app.run(port=1234) diff --git a/examples/nylas-connect/credentials.py.template b/examples/nylas-connect/credentials.py.template new file mode 100644 index 00000000..6fb22262 --- /dev/null +++ b/examples/nylas-connect/credentials.py.template @@ -0,0 +1,4 @@ +GOOGLE_CLIENT_ID = '' +GOOGLE_CLIENT_SECRET = '' +NYLAS_CLIENT_ID = '' +NYLAS_CLIENT_SECRET = '' diff --git a/examples/nylas-connect/readme.md b/examples/nylas-connect/readme.md new file mode 100644 index 00000000..5fcadac6 --- /dev/null +++ b/examples/nylas-connect/readme.md @@ -0,0 +1,103 @@ +# Nylas Connect + +This tiny flask app is a simple example of how to use Nylas' [Native +authentication APIs](https://www.nylas.com/docs/platform#native_authentication). +It shows how to receive a `refresh_token` from Google before authenticating with +Nylas. Then it uses the Nylas Python SDK to connect an email account and +load the user's latest email. + +While this steps through the Google OAuth flow manually, you can alternatively +use Google API SDKs for Python and many other languages. Learn more about that +[here](https://developers.google.com/api-client-library/python/). You can also +learn more about Google OAuth +[here](https://developers.google.com/identity/protocols/OAuth2WebServer). + +Here is an overview of the complete flow: + +``` +Your App Google ++------+ +-----+ +| | Redirect user to Oauth Login | | +| +----------------------------------> | | +| | | | +| | Authorization code | | +| | <--(localhost)-<-(ngrok)-----------+ | +| | | | +| | Request refresh token | | +| +----------------------------------> | | +| | | | +| | Refresh & access token | | +| | <----------------------------------+ | +| | +-----+ +| | +| | +| | Nylas +| | Request Authorization code +-----+ +| +----------------------------------> | | +| | | | +| | Authorization code | | +| | <----------------------------------+ | +| | | | +| | Request Access Token | | +| +----------------------------------> | | +| | | | +| | Access Token | | +| | <----------------------------------+ | ++------+ +-----+ +``` + + +# Getting Started + +## Dependencies + +### Google Application + +You'll need to have a Nylas [developer account](https://developer.nylas.com), a +Google Application, and the respective `client_id` and `client_secret`s. +Learn about how to setup the Google App to correctly work with Nylas +[here](https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide). + +### ngrok + +[ngrok](https://ngrok.com/) makes it really easy to test callback urls that are +running locally on your computer. + +### virtualenv + +Make sure `virtualenv` is installed. To install it type the following: + +```bash +pip install virtualenv +``` + +## Initial Setup + +Add your google and nylas client id's and secrets to a new file credentials.py. +See `credentials.py.template` for an example + + +Create a virtual env, activate it, and then install python dependencies + +```bash +virtualenv env +source env/bin/activate +pip install -r requirements.txt +``` + +# Running the app + +First, make sure ngrok is running with the same port that the local flask app is +running. + +```bash +ngrok http 1234 +``` + +Next, run the flask app. + +```bash +./app.py +``` + +Visit http://localhost:1234 in your browser. diff --git a/examples/nylas-connect/requirements.txt b/examples/nylas-connect/requirements.txt new file mode 100644 index 00000000..126ccf02 --- /dev/null +++ b/examples/nylas-connect/requirements.txt @@ -0,0 +1,19 @@ +bumpversion==0.5.3 +cffi==1.7.0 +click==6.6 +cryptography==1.4 +enum34==1.1.6 +Flask==0.11.1 +idna==2.1 +ipaddress==1.0.16 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 +ndg-httpsclient==0.4.2 +nylas==1.2.3 +pyasn1==0.1.9 +pycparser==2.14 +pyOpenSSL==16.0.0 +requests==2.11.0 +six==1.10.0 +Werkzeug==0.11.10 diff --git a/examples/nylas-connect/templates/index.html b/examples/nylas-connect/templates/index.html new file mode 100644 index 00000000..1b17154c --- /dev/null +++ b/examples/nylas-connect/templates/index.html @@ -0,0 +1,21 @@ + + + + + + Nylas Connect + + + + + + + + +

Nylas Connect would like to authorize your email!

+ Click to continue + + + From e7e8051613f2c914d7e0d598f24c450f2b63d4bc Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Thu, 25 Aug 2016 10:32:54 -0700 Subject: [PATCH 005/451] Adds python webhooks example Test Plan: none Reviewers: khamidou Reviewed By: khamidou Differential Revision: https://phab.nylas.com/D3212 --- examples/webhooks/.gitignore | 3 + examples/webhooks/app.py | 76 +++++++++++++++++++++++ examples/webhooks/credentials.py.template | 1 + examples/webhooks/readme.md | 49 +++++++++++++++ examples/webhooks/requirements.txt | 7 +++ 5 files changed, 136 insertions(+) create mode 100644 examples/webhooks/.gitignore create mode 100755 examples/webhooks/app.py create mode 100644 examples/webhooks/credentials.py.template create mode 100644 examples/webhooks/readme.md create mode 100644 examples/webhooks/requirements.txt diff --git a/examples/webhooks/.gitignore b/examples/webhooks/.gitignore new file mode 100644 index 00000000..cb272b17 --- /dev/null +++ b/examples/webhooks/.gitignore @@ -0,0 +1,3 @@ +credentials.py +env/ +*.pyc diff --git a/examples/webhooks/app.py b/examples/webhooks/app.py new file mode 100755 index 00000000..f69dd493 --- /dev/null +++ b/examples/webhooks/app.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +import json +import flask +import requests +import hmac +import hashlib + +try: + from credentials import * +except ImportError: + log.error("Couldn't import credentials.py --- you'll need to create it.") + log.error("See credentials.py.template for more details.") + sys.exit(-1) + +app = flask.Flask(__name__) + + +@app.route('/webhook', methods=['GET', 'POST']) +def index(): + # Nylas will check to make sure your webhook is valid by making a GET + # request to your endpoint with a challenge parameter when you add the + # endpoint to the developer dashboard. All you have to do is return the + # value of the challenge parameter in the body of the response. + if flask.request.method == 'GET' and 'challenge' in flask.request.args: + return flask.request.args['challenge'] + # Nylas sent us a webhook notification for some kind of event, so we should + # process it! + elif flask.request.method == 'POST': + # Verify the request to make sure it's actually from Nylas. + if not verify_request(flask.request): + return "X-Nylas-Signature failed verification", 401 + # Nylas will send us a json object of the deltas. + data = json.loads(flask.request.data) + for delta in data['deltas']: + # Print some of the information Nylas sent us. This is where you + # would normally process the webhook notification and do things like + # fetch relevant message ids, update your database, etc. + print "{} at {} with id {}".format(delta['type'], delta['date'], + delta['object_data']['id']) + # Don't forget to let Nylas know that everything was pretty ok. + return "Success", 200 + + else: + # We only allow GET and POST requests to this endpoint. + return "Method not allowed", 405 + + +# Each request made by Nylas includes an X-Nylas-Signature header. The header +# contains the HMAC-SHA256 signature of the request body, using your client +# secret as the signing key. This allows your app to verify that the +# notification really came from Nylas. +def verify_request(request): + digest = hmac.new(NYLAS_CLIENT_SECRET, msg=request.data, digestmod=hashlib.sha256).hexdigest() + return digest == request.headers.get('X-Nylas-Signature') + + +# Setup ngrok settings to ensure everything works locally +def initialize(): + # Make sure ngrok is running + try: + resp = requests.get('http://localhost:4040/api/tunnels').json() + except requests.exceptions.ConnectionError: + print "It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'" + sys.exit(-1) + + global WEBHOOK_URI + WEBHOOK_URI = "{}/webhook".format(resp['tunnels'][1]['public_url']) + + +if __name__ == '__main__': + import uuid + initialize() + app.secret_key = str(uuid.uuid4()) + app.debug = False + print "{}\nAdd the above url to the webhooks page at https://developer.nylas.com".format(WEBHOOK_URI) + app.run(port=1234) diff --git a/examples/webhooks/credentials.py.template b/examples/webhooks/credentials.py.template new file mode 100644 index 00000000..f61e903b --- /dev/null +++ b/examples/webhooks/credentials.py.template @@ -0,0 +1 @@ +NYLAS_CLIENT_SECRET = '' diff --git a/examples/webhooks/readme.md b/examples/webhooks/readme.md new file mode 100644 index 00000000..9a81e82c --- /dev/null +++ b/examples/webhooks/readme.md @@ -0,0 +1,49 @@ +# Nylas Webhooks + +This tiny flask app is a simple example of how to use Nylas' webhooks feature. +This app correctly responds to Nylas' challenge request when you add a webhook +url to the [developer dashboard](https://developer.nylas.com). It also verifies +any webhook notification POST requests by Nylas and prints out some information +about the notification. + +# Dependencies + +## ngrok + +[ngrok](https://ngrok.com/) makes it really easy to test callback urls that are +running locally on your computer. + +## virtualenv + +Make sure `virtualenv` is installed. To install it type the following: + +```bash +pip install virtualenv +``` + +# Initial Setup + +Create a virtual env, activate it, and then install python dependencies + +```bash +virtualenv env +source env/bin/activate +pip install -r requirements.txt +``` + +# Running the app + +First, make sure ngrok is running with the same port that the local flask app is +running. + +```bash +ngrok http 1234 +``` + +Next, run the flask app. + +```bash +./app.py +``` + +Follow the instructions that are printed to the console. diff --git a/examples/webhooks/requirements.txt b/examples/webhooks/requirements.txt new file mode 100644 index 00000000..ce725103 --- /dev/null +++ b/examples/webhooks/requirements.txt @@ -0,0 +1,7 @@ +click==6.6 +Flask==0.11.1 +itsdangerous==0.24 +Jinja2==2.8 +MarkupSafe==0.23 +requests==2.11.1 +Werkzeug==0.11.10 From bd90c5392d467d8527f0fcc41ecc5e7e44f0cf26 Mon Sep 17 00:00:00 2001 From: Matt Gilson Date: Fri, 21 Oct 2016 18:09:04 -0400 Subject: [PATCH 006/451] Empty dicts are falsy. Non-empty dicts are truthy. (#33) This makes statements like the following inefficient -- particularly on python2.x where `dict.keys()` needs to build a new list. ```py if len(some_dict.keys()) > 0: ... ``` --- nylas/client/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 5cfc9759..0929d14e 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -284,7 +284,7 @@ def _get_resource_data(self, cls, id, def _create_resource(self, cls, data, **kwargs): url = "{}/{}/".format(self.api_server, cls.collection_name) - if len(kwargs.keys()) > 0: + if kwargs: url = "{}?{}".format(url, urlencode(kwargs)) session = self._get_http_session(cls.api_root) @@ -323,7 +323,7 @@ def _delete_resource(self, cls, id, data=None, **kwargs): name = cls.collection_name url = "{}/{}/{}".format(self.api_server, name, id) - if len(kwargs.keys()) > 0: + if kwargs: url = "{}?{}".format(url, urlencode(kwargs)) session = self._get_http_session(cls.api_root) if data: @@ -336,7 +336,7 @@ def _update_resource(self, cls, id, data, **kwargs): name = cls.collection_name url = "{}/{}/{}".format(self.api_server, name, id) - if len(kwargs.keys()) > 0: + if kwargs: url = "{}?{}".format(url, urlencode(kwargs)) session = self._get_http_session(cls.api_root) From 15576c798816138c43245757d526328084ca3019 Mon Sep 17 00:00:00 2001 From: Matt Gilson Date: Fri, 21 Oct 2016 18:14:21 -0400 Subject: [PATCH 007/451] Don't import `urlencode` 3 times --- nylas/client/client.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 0929d14e..54b7d420 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -1,10 +1,6 @@ import sys import requests import json -try: - from urllib import urlencode -except ImportError: - from urllib.parse import urlencode from os import environ from base64 import b64encode from six.moves.urllib.parse import urlencode From ae1c26612dd1eb549ff33af8239e28c0c0df4c2b Mon Sep 17 00:00:00 2001 From: Muhammad Waqas Javed Date: Thu, 27 Oct 2016 04:45:58 +0500 Subject: [PATCH 008/451] Fixing URL for nylas authentication docs (#37) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06a7b81d..d2b18a35 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Here's how it works: 3. She is redirected to a callback URL of your own, along with an access code 4. You use this access code to get an authorization token to the API -For more information about authenticating with Nylas, visit the [Developer Documentation](https://www.nylas.com/docs/gettingstarted-hosted#authenticating). +For more information about authenticating with Nylas, visit the [Developer Documentation](https://www.nylas.com/cloud/docs#authentication). In practice, the Nylas REST API client simplifies this down to two steps. From 679efc465f15f207dafccb6e1d36dbc28f383aa0 Mon Sep 17 00:00:00 2001 From: Matt Gilson Date: Wed, 26 Oct 2016 19:53:32 -0400 Subject: [PATCH 009/451] Fix Event.as_json to prevent mutation of `Event` (#35) Currently, If I try to get an `Event` as json, then the event will thereafter have the `event.when['object']` removed. e.g. ```py event = client.events.find(event_id) print(event.when['object']) json_event = event.as_json() print(event.when['object']) # KeyError! Snap! ``` * Also, I decided to use `dict.pop(key, None)` instead of `if key in d: del d[key]` because it looks nicer (IMHO) * and `if d.get(key)` rather than `if key in d` since the first one will work as we expect if `d[key] == None` whereas the latter won't. --- nylas/client/restful_models.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 95194906..04ee8fe7 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -408,9 +408,13 @@ def __init__(self, api): def as_json(self): dct = NylasAPIObject.as_json(self) # Filter some parameters we got from the API - if 'when' in dct: - if 'object' in dct['when']: - del dct['when']['object'] + 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 From a722b0122b81c8a221a969ad23b9651fe5cc7c8b Mon Sep 17 00:00:00 2001 From: Christine Spang Date: Mon, 3 Apr 2017 14:10:50 -0700 Subject: [PATCH 010/451] Fix error in webhooks example when ngrok is not running --- examples/webhooks/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/webhooks/app.py b/examples/webhooks/app.py index f69dd493..80836a05 100755 --- a/examples/webhooks/app.py +++ b/examples/webhooks/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import sys import json import flask import requests From 085615fad116591136e1196aa3ab16f72a3cad7f Mon Sep 17 00:00:00 2001 From: Matt Gilson Date: Wed, 26 Oct 2016 11:46:29 -0700 Subject: [PATCH 011/451] Add message_id to Event NylasAPIObject The `message_id` field seems to be missing from the documentation, but it seems that it is present when the event was created via email and it is not present with the event is a true calendar event. With this parameter, users can filter out "Invite"/"Accepted" events that all reference the same actual calendar event. --- nylas/client/restful_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 04ee8fe7..73bd53a5 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -399,7 +399,7 @@ class Event(NylasAPIObject): attrs = ["id", "account_id", "title", "description", "location", "read_only", "when", "busy", "participants", "calendar_id", "recurrence", "status", "master_event_id", "owner", - "original_start_time", "object"] + "original_start_time", "object", "message_id"] collection_name = 'events' def __init__(self, api): From 7566ff6e9bb42f8583b671194259df2817161f0f Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 16 May 2017 13:53:32 -0700 Subject: [PATCH 012/451] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d2b18a35..33af7bb6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Python bindings for the Nylas REST API. https://www.nylas.com/docs This library is available on pypi. You can install it by running `pip install nylas`. -##Requirements +## Requirements - requests (>= 2.4.2) From e6230c357d43764a59eb15ea22c2ea2222cbbcba Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 16 May 2017 15:34:49 -0700 Subject: [PATCH 013/451] Add ability to revoke token. Fixes #39 --- README.md | 10 ++++++++++ nylas/client/client.py | 14 +++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 33af7bb6..3ac52010 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,16 @@ def login_callback(): You can take a look at [examples/server.py](examples/server.py) to see a server implementing the auth flow. +**Revoke a token** + +To revoke an access token and remove it from your APIClient's session you can +use the `revoke_token` method on APIClient + +```python + client.revoke_token() +``` + + ### Connecting to an account ```python diff --git a/nylas/client/client.py b/nylas/client/client.py index 54b7d420..d68452b5 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -93,6 +93,7 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), self.api_server = api_server self.authorize_url = api_server + '/oauth/authorize' self.access_token_url = api_server + '/oauth/token' + self.revoke_url = api_server + '/oauth/revoke' self.app_secret = app_secret self.app_id = app_id @@ -153,8 +154,8 @@ def token_for_code(self, code): resp = requests.post(self.access_token_url, data=urlencode(args), headers=headers).json() - self.auth_token = resp[u'access_token'] - return self.auth_token + self.access_token = resp[u'access_token'] + return self.access_token def is_opensource_api(self): if self.app_id is None and self.app_secret is None: @@ -162,6 +163,13 @@ def is_opensource_api(self): return False + @nylas_excepted + def revoke_token(self): + resp = self.session.post(self.revoke_url) + _validate(resp) + self.auth_token = None + self.access_token = None + @property def account(self): return self._get_resource(SingletonAccount, '') @@ -344,7 +352,7 @@ def _update_resource(self, cls, id, data, **kwargs): @nylas_excepted def _call_resource_method(self, cls, id, method_name, data): - """POST a dictionnary to an API method, + """POST a dictionary to an API method, for example /a/.../accounts/id/upgrade""" name = cls.collection_name if cls.api_root != 'a': From 76e3bf47c614f26b5dfeef9df9b5a8b31f343144 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 16 May 2017 15:33:38 -0700 Subject: [PATCH 014/451] Add token revocation example. Use local nylas module --- examples/nylas-connect/app.py | 24 +++++++++++++++--------- examples/nylas-connect/requirements.txt | 1 - 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/examples/nylas-connect/app.py b/examples/nylas-connect/app.py index e5ab2ba8..abdf1cec 100755 --- a/examples/nylas-connect/app.py +++ b/examples/nylas-connect/app.py @@ -8,6 +8,8 @@ import logging import subprocess +sys.path.append('../../') + from nylas import APIClient # Sets the logging format @@ -65,6 +67,17 @@ def index(): return client.threads.first().messages.first().body +@app.route('/revoke') +def revoke_token(): + client = APIClient(NYLAS_CLIENT_ID, NYLAS_CLIENT_SECRET, flask.session['nylas_access_token']) + client.revoke_token() + + del flask.session['google_credentials'] + del flask.session['nylas_access_token'] + + return "Token revoked" if client.access_token is None else "Failed token revocation" + + # This is the url Google will call once a user has approved access to their # account @app.route('/oauth2callback') @@ -157,17 +170,10 @@ def nylas_token(data): raise Exception("Error getting access token from Nylas", err=resp) -# Setup ngrok and google developer settings to ensure everything works locally +# Setup google developer settings to ensure everything works locally def initialize(): - # Make sure ngrok is running - try: - resp = requests.get('http://localhost:4040/api/tunnels').json() - except requests.exceptions.ConnectionError: - print "It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'" - sys.exit(-1) - global REDIRECT_URI - REDIRECT_URI = "{}/oauth2callback".format(resp['tunnels'][0]['public_url']) + REDIRECT_URI = "{}/oauth2callback".format("http://lvh.me:1234") print REDIRECT_URI s = raw_input("Have you added the url above as an authorized callback " "in Google's Developer console? y/n ") diff --git a/examples/nylas-connect/requirements.txt b/examples/nylas-connect/requirements.txt index 126ccf02..d30bdc7f 100644 --- a/examples/nylas-connect/requirements.txt +++ b/examples/nylas-connect/requirements.txt @@ -10,7 +10,6 @@ itsdangerous==0.24 Jinja2==2.8 MarkupSafe==0.23 ndg-httpsclient==0.4.2 -nylas==1.2.3 pyasn1==0.1.9 pycparser==2.14 pyOpenSSL==16.0.0 From 4deff685f5a61086b6525e666d104a78c979c107 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Tue, 16 May 2017 15:48:08 -0700 Subject: [PATCH 015/451] Respect the offset parameter for restfulmodelcollection when updating filters. Fixes #32 --- nylas/client/restful_model_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nylas/client/restful_model_collection.py b/nylas/client/restful_model_collection.py index 38308c75..6a4df0e5 100644 --- a/nylas/client/restful_model_collection.py +++ b/nylas/client/restful_model_collection.py @@ -10,7 +10,7 @@ def __init__(self, cls, api, filter={}, offset=0, if not isinstance(api, APIClient): raise Exception("Provided api was not an APIClient.") - filters.setdefault('offset', 0) + filters.setdefault('offset', offset) self.model_class = cls self.filters = filters From 4357111b6abe505623e9bc9fb3a8007066a0a72d Mon Sep 17 00:00:00 2001 From: Christine Spang Date: Tue, 16 May 2017 17:09:10 -0700 Subject: [PATCH 016/451] Don't ruby in the python README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ac52010..1d17d7c2 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ account_id = client.accounts.first().id # Display the contents of the first message for the first account client = APIClient(None, None, account_id, 'http://localhost:5555/') -puts client.messages.first().body +print client.messages.first().body ``` From fd86c2e702fa8622e16760bdebd2458e1ec4f193 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Wed, 17 May 2017 15:04:21 -0700 Subject: [PATCH 017/451] Send correct auth headers for account management Fix system.py tests for listing accounts for an application vs listing a single authenticated account connected to the APIClient Fixes #11 --- nylas/client/client.py | 5 +++-- nylas/client/restful_models.py | 13 ++++++++----- tests/system.py | 12 ++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index d68452b5..4135460a 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -113,8 +113,9 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), self.admin_session = requests.Session() if app_secret is not None: - self.admin_session.headers = {'Authorization': 'Bearer ' + - app_secret, + b64_app_secret = b64encode(app_secret + ':') + self.admin_session.headers = {'Authorization': 'Basic ' + + b64_app_secret, 'X-Nylas-API-Wrapper': 'python', 'User-Agent': version_header} diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 73bd53a5..8826627d 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -434,8 +434,8 @@ def child_collection(self, cls, **filters): class Account(NylasAPIObject): api_root = 'a' - attrs = ["account_id", "trial", "trial_expires", "sync_state", - "billing_state", "account_id"] + attrs = ['account_id', 'billing_state', 'email', 'id', 'namespace_id', + 'sync_state', 'trial'] collection_name = 'accounts' @@ -454,12 +454,15 @@ def downgrade(self): self.api._call_resource_method(self, self.account_id, 'downgrade', None) + def delete(self): + raise NotImplementedError + class APIAccount(NylasAPIObject): - attrs = ["email_address", "id", "account_id", "object", - "provider", "name", "organization_unit"] + attrs = ['account_id', 'email_address', 'id', 'name', 'object', + 'organization_unit', 'provider', 'sync_state'] - collection_name = 'accounts' + collection_name = 'account' def __init__(self, api): NylasAPIObject.__init__(self, APIAccount, api) diff --git a/tests/system.py b/tests/system.py index 3caf3f81..86b77fbe 100644 --- a/tests/system.py +++ b/tests/system.py @@ -14,9 +14,13 @@ count = 0 -print "Listing accounts" +print "Listing accounts for application" for account in client.accounts: - print (account.email_address, account.provider) + print (account.email, account.id, account.billing_state, account.sync_state) + +print "Listing authenticated API account" +account = client.account +print (account.email_address, account.provider, account.id) print 'Marking the first thread as unread' th = client.threads.where({'in': 'inbox'}).first() @@ -32,7 +36,7 @@ print "Sending an email" draft = client.drafts.create() -draft.to = [{'name': 'Python SDK test', 'email': 'karim@nylas.com'}] +draft.to = [{'name': 'Python SDK test', 'email': 'inboxapptest415@gmail.com'}] draft.subject = "Python SDK test" draft.body = "Stay polish, stay hungary" draft.send() @@ -47,7 +51,7 @@ ev.when = {"start_time": time.mktime(d1.timetuple()), "end_time": time.mktime(d2.timetuple())} ev.location = "The Old Ritz" -ev.participants = [{'name': 'Karim Hamidou', 'email': 'karim@nylas.com'}] +ev.participants = [{'name': 'Nylas Test', 'email': 'inboxapptest415@gmail.com'}] ev.calendar_id = calendar.id ev.save(notify_participants='true') From aed57616e0268322236e031bb8446da9779005ca Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Wed, 17 May 2017 15:39:52 -0700 Subject: [PATCH 018/451] Add docs showing how to send rsvps during event creation Fixes #18 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d17d7c2..2a73356d 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ ev.when = {"start_time": 1416423667, "end_time": 1416448867} # These numbers are ev.location = "The Old Ritz" ev.participants = [{"name": "My Friend", 'email': 'my.friend@example.com'}] ev.calendar_id = calendar.id -ev.save() +ev.save(notify_participants='true') # notify_participants is sent as a query parameter in the request # Update it ev.location = "The Waldorf-Astoria" From d92140b8a357bcde5b47cca9867703688eb106e5 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Wed, 17 May 2017 15:45:14 -0700 Subject: [PATCH 019/451] Remove deprecated inbox name --- inbox/__init__.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 inbox/__init__.py diff --git a/inbox/__init__.py b/inbox/__init__.py deleted file mode 100644 index 1830fba7..00000000 --- a/inbox/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from nylas import * From d7a42dd9d51672c46f7ef06644cf1283e07c31e3 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Wed, 17 May 2017 16:27:03 -0700 Subject: [PATCH 020/451] Add message tracking feature for draft sending --- README.md | 20 ++++++++++++++++++++ nylas/client/restful_models.py | 2 +- tests/system.py | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a73356d..d9827f46 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,26 @@ draft.delete() # client.drafts.delete(draft.id, {'version': draft.version}) ``` +### Message Tracking Features + +If you have webhooks enabled for your developer application you can now access Nylas' [Message Tracking Features](https://www.nylas.com/docs/platform#enabling_tracking) to see when a recipient opens, replies, or clicks links within a message. + +```python +# Send a message with tracking enabled +draft = client.drafts.create() +draft.to = [{'name': 'Python SDK open tracking test', 'email': 'inboxapptest415@gmail.com'}] +draft.subject = "Python SDK open tracking test" +draft.body = "Stay polish, stay hungary" +draft.tracking = { 'links': 'false', 'opens': 'true', 'thread_replies': 'true', 'payload':'python sdk open tracking test' } +draft.send() +``` + +It’s important to note that you must wrap links in `` tags for them to be +tracked. Most email clients automatically “linkify” things that look like links, +like `10.0.0.1`, `tel:5402502334`, or `apple.com`. For links to be tracked +properly, you must linkify the content before sending the draft. + + ### Working with Events The following example shows how to create and update an event. diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 8826627d..f9d564d0 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -307,7 +307,7 @@ 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"] + "reply_to", "starred", "snippet", "tracking"] collection_name = 'drafts' def __init__(self, api, thread_id=None): diff --git a/tests/system.py b/tests/system.py index 86b77fbe..7e092185 100644 --- a/tests/system.py +++ b/tests/system.py @@ -55,6 +55,15 @@ ev.calendar_id = calendar.id ev.save(notify_participants='true') +# Send a message with tracking enabled +print 'Send a message with open tracking enabled' +draft = client.drafts.create() +draft.to = [{'name': 'Python SDK open tracking test', 'email': 'inboxapptest415@gmail.com'}] +draft.subject = "Python SDK open tracking test" +draft.body = "Stay polish, stay hungary" +draft.tracking = { 'links': 'false', 'opens': 'true', 'thread_replies': 'true', 'payload':'python sdk open tracking test' } +draft.send() + print 'Listing folders' for label in client.labels: print label.display_name From 7849e8d7bf22be7e6a7ac47bb6b523a6e3424a33 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Wed, 17 May 2017 17:14:44 -0700 Subject: [PATCH 021/451] Add support for expanded message view Fixes #20 --- README.md | 8 ++++++++ nylas/client/restful_models.py | 3 ++- tests/system.py | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d9827f46..4841a1d8 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,14 @@ for message in thread.messages.items(): print message.raw ``` +To get the [expanded message view](https://www.nylas.com/docs/platform#expanded_message_view) that includes convenient header information, include `view='expanded'` in the where clause +``` +message = client.messages.where(in_='inbox', view='expanded').first() +message.headers['Message-Id'] +message.headers['References'] +message.headers['In-Reply-To'] +``` + ### Working with Folders and Labels The Folders and Labels API replaces the now deprecated Tags API. For Gmail accounts, this API allows you to apply labels to whole threads or individual messages. For providers other than Gmail, you can move threads and messages between folders -- a message can only belong to one folder. diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index f9d564d0..7245f450 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -80,7 +80,8 @@ def update(self): 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"] + "thread_id", "to", "unread", "starred", "_folder", "_labels", + "headers"] collection_name = 'messages' def __init__(self, api): diff --git a/tests/system.py b/tests/system.py index 7e092185..e98e87a7 100644 --- a/tests/system.py +++ b/tests/system.py @@ -64,6 +64,10 @@ draft.tracking = { 'links': 'false', 'opens': 'true', 'thread_replies': 'true', 'payload':'python sdk open tracking test' } draft.send() +print "Get expanded view for message" +m = client.messages.where(in_='inbox', limit=1, view='expanded').first() +print m.headers['Message-Id'] + print 'Listing folders' for label in client.labels: print label.display_name From d815d01758f46b18c992e57a19a0942fbedfdecf Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Thu, 18 May 2017 10:39:58 -0700 Subject: [PATCH 022/451] Bump version and prep 2.0.0 release --- .bumpversion.cfg | 2 +- CHANGELOG.md | 13 +++++++++++++ nylas/_client_sdk_version.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3188362b..a993b482 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = True -current_version = 1.2.3 +current_version = 2.0.0 [bumpversion:file:nylas/_client_sdk_version.py] diff --git a/CHANGELOG.md b/CHANGELOG.md index 37aea251..e845c805 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ nylas-python Changelog ====================== +v2.0.0 +------ + +Release May 18, 2017: +- Add support for expanded message view +- Remove deprecated "Inbox" name +- Send correct auth headers for account management +- Respect the offset parameter for restfulmodelcollection +- Add ability to revoke token + +[Full Changelog](https://github.com/nylas/nylas-ruby/compare/v1.2.3...v2.0.0) + + v1.2.3 ------ diff --git a/nylas/_client_sdk_version.py b/nylas/_client_sdk_version.py index 02357ebb..38964004 100644 --- a/nylas/_client_sdk_version.py +++ b/nylas/_client_sdk_version.py @@ -1 +1 @@ -__VERSION__ = "1.2.3" +__VERSION__ = "2.0.0" From 99de25509cd08f368d408c448396677c6c025970 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 08:22:25 -0400 Subject: [PATCH 023/451] Measure code coverage --- .coveragerc | 2 ++ .travis.yml | 6 +++++- README.md | 3 ++- setup.py | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..8460a941 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +source = nylas diff --git a/.travis.yml b/.travis.yml index 759ca256..13a85b19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,9 @@ language: python python: - "2.7" -install: "python setup.py install" +install: + - "python setup.py install" + - "travis_retry pip install codecov" script: "python setup.py test" +after_success: + - "codecov" diff --git a/README.md b/README.md index 4841a1d8..7325d6fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Nylas REST API Python bindings ![Travis build status](https://travis-ci.org/nylas/nylas-python.svg?branch=master) +# Nylas REST API Python bindings ![Travis build status](https://travis-ci.org/nylas/nylas-python.svg?branch=master) [![Code Coverage](https://codecov.io/gh/nylas/nylas-python/branch/master/graph/badge.svg)](https://codecov.io/gh/nylas/nylas-python) + Python bindings for the Nylas REST API. https://www.nylas.com/docs diff --git a/setup.py b/setup.py index 504a3ea5..282cfff8 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ class PyTest(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) - self.pytest_args = '--junitxml ./tests/output tests/' + self.pytest_args = '--cov --junitxml ./tests/output tests/' def finalize_options(self): TestCommand.finalize_options(self) @@ -56,7 +56,7 @@ def main(): "pyasn1", ], dependency_links=[], - tests_require=["pytest", "coverage", "responses", "httpretty"], + tests_require=["pytest", "pytest-cov", "responses", "httpretty"], cmdclass={'test': PyTest}, author="Nylas Team", author_email="support@nylas.com", From b00fa6a0b0ee940aa365ea1daf273bba5b7573cd Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 08:27:00 -0400 Subject: [PATCH 024/451] Remove manual tests Manual tests are difficult to run and difficult to verify. It's better to not even have them in the codebase. --- README.md | 15 ++----- tests/credentials.py.template | 3 -- tests/oauth.py | 51 ------------------------ tests/system.py | 73 ----------------------------------- 4 files changed, 3 insertions(+), 139 deletions(-) delete mode 100644 tests/credentials.py.template delete mode 100644 tests/oauth.py delete mode 100644 tests/system.py diff --git a/README.md b/README.md index 7325d6fb..9798fe4c 100644 --- a/README.md +++ b/README.md @@ -381,21 +381,12 @@ Please sign the [Contributor License Agreement](https://nylas.com/cla.html) befo ### Releasing a new version -We have a three-step process for releasing a new version of the Python SDK. Remember that people depend on this library not breaking, so don't cut corners. +We have a two-step process for releasing a new version of the Python SDK. Remember that people depend on this library not breaking, so don't cut corners. 1. Run the unit tests. `python setup.py test` -2. Run the "system" tests. They use a live server to check that everything works as expected, so you'll need to have a valid Nylas application id and secret. - In the tests directory you'll find a file named `tests/credentials.py.template`. Rename it into `credentials.py` and change the APP_ID and APP_SECRET to your own app id and secret. - - Run the tests: - - ```shell - PYTHONPATH=/your-sdk-path python tests/oauth.py - PYTHONPATH=/your-sdk-path python tests/system.py - ``` - -3. Finally, you can create a new release by doing: + +2. Create a new release by doing: ```shell python setup.py release diff --git a/tests/credentials.py.template b/tests/credentials.py.template deleted file mode 100644 index f445e0f0..00000000 --- a/tests/credentials.py.template +++ /dev/null @@ -1,3 +0,0 @@ -APP_ID = 'APP ID' -APP_SECRET = 'APP SECRET' -AUTH_TOKEN = 'local' # The access token to a local account (i.e: running on your machine). diff --git a/tests/oauth.py b/tests/oauth.py deleted file mode 100644 index 4571de18..00000000 --- a/tests/oauth.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -import time -from flask import Flask, url_for, session, request, redirect, Response - -from nylas import APIClient - -try: - from credentials import APP_ID, APP_SECRET -except ImportError: - print "Couldn't import credentials.py --- you'll need to create it." - print "See credentials.py.template for more details." - sys.exit(-1) - -app = Flask(__name__) -app.debug = True -app.secret_key = 'secret' - -assert APP_ID != 'YOUR_APP_ID' or APP_SECRET != 'YOUR_APP_SECRET',\ - "You should change the value of APP_ID and APP_SECRET" - - -@app.route('/') -def index(): - # We don't have an access token, so we're going to use OAuth to - # authenticate the user - - # Ask flask to generate the url corresponding to the login_callback - # route. This is similar to using reverse() in django. - redirect_uri = url_for('.login_callback', _external=True) - - client = APIClient(APP_ID, APP_SECRET) - return redirect(client.authentication_url(redirect_uri)) - - -@app.route('/login_callback') -def login_callback(): - if 'error' in request.args: - return "Login error: {0}".format(request.args['error']) - - # Exchange the authorization code for an access token - client = APIClient(APP_ID, APP_SECRET) - code = request.args.get('code') - token = client.token_for_code(code) - return token - -if __name__ == '__main__': - print "\033[94mOauth self-test. Please browse to http://localhost:5555 and make\033[0m" - print "\033[94msure that you're seeing a valid API token.\033[0m" - - app.run(host='0.0.0.0', port=5555) diff --git a/tests/system.py b/tests/system.py deleted file mode 100644 index e98e87a7..00000000 --- a/tests/system.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import re -import pytest -import time -import datetime -import sys -from nylas import APIClient -from nylas.client.restful_models import Label, Folder -from nylas.client.errors import * -from credentials import APP_ID, APP_SECRET, AUTH_TOKEN - -client = APIClient(APP_ID, APP_SECRET, AUTH_TOKEN) - -count = 0 - -print "Listing accounts for application" -for account in client.accounts: - print (account.email, account.id, account.billing_state, account.sync_state) - -print "Listing authenticated API account" -account = client.account -print (account.email_address, account.provider, account.id) - -print 'Marking the first thread as unread' -th = client.threads.where({'in': 'inbox'}).first() -print th.subject -th.mark_as_unread() - -print "Displaying 10 thread subjects" -for thread in client.threads.items(): - print thread.subject - count += 1 - if count == 10: - break - -print "Sending an email" -draft = client.drafts.create() -draft.to = [{'name': 'Python SDK test', 'email': 'inboxapptest415@gmail.com'}] -draft.subject = "Python SDK test" -draft.body = "Stay polish, stay hungary" -draft.send() - -print 'Creating an event' -calendar = filter(lambda cal: not cal.read_only, client.calendars)[0] -ev = client.events.create() -ev.title = "Party at the Ritz" - -d1 = datetime.datetime.now() + datetime.timedelta(days=5,hours=4) -d2 = datetime.datetime.now() + datetime.timedelta(days=5,hours=5) - -ev.when = {"start_time": time.mktime(d1.timetuple()), "end_time": time.mktime(d2.timetuple())} -ev.location = "The Old Ritz" -ev.participants = [{'name': 'Nylas Test', 'email': 'inboxapptest415@gmail.com'}] -ev.calendar_id = calendar.id -ev.save(notify_participants='true') - -# Send a message with tracking enabled -print 'Send a message with open tracking enabled' -draft = client.drafts.create() -draft.to = [{'name': 'Python SDK open tracking test', 'email': 'inboxapptest415@gmail.com'}] -draft.subject = "Python SDK open tracking test" -draft.body = "Stay polish, stay hungary" -draft.tracking = { 'links': 'false', 'opens': 'true', 'thread_replies': 'true', 'payload':'python sdk open tracking test' } -draft.send() - -print "Get expanded view for message" -m = client.messages.where(in_='inbox', limit=1, view='expanded').first() -print m.headers['Message-Id'] - -print 'Listing folders' -for label in client.labels: - print label.display_name From 13512c953d5ab79e5a3cd3aae197f3d1812831d0 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 08:45:49 -0400 Subject: [PATCH 025/451] Fixing imports * Replace relative imports with absolute imports * Replace `import *` with explicit imports * Remove unused imports * Move imports in `__name__ == '__main__'` section to top of file --- examples/nylas-connect/app.py | 4 ++-- examples/webhooks/app.py | 4 ++-- nylas/client/client.py | 23 +++++++++++++---------- nylas/client/restful_models.py | 6 ++---- nylas/client/util.py | 1 - tests/conftest.py | 1 - tests/test_folder_labels.py | 2 -- tests/test_send_error_handling.py | 4 +++- 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/examples/nylas-connect/app.py b/examples/nylas-connect/app.py index abdf1cec..ca81ae03 100755 --- a/examples/nylas-connect/app.py +++ b/examples/nylas-connect/app.py @@ -7,6 +7,7 @@ import urllib import logging import subprocess +import uuid sys.path.append('../../') @@ -170,7 +171,7 @@ def nylas_token(data): raise Exception("Error getting access token from Nylas", err=resp) -# Setup google developer settings to ensure everything works locally +# Setup google developer settings to ensure everything works locally def initialize(): global REDIRECT_URI REDIRECT_URI = "{}/oauth2callback".format("http://lvh.me:1234") @@ -185,7 +186,6 @@ def initialize(): if __name__ == '__main__': logging.info("Initializing Application") - import uuid initialize() app.secret_key = str(uuid.uuid4()) app.debug = False diff --git a/examples/webhooks/app.py b/examples/webhooks/app.py index 80836a05..1e56f337 100755 --- a/examples/webhooks/app.py +++ b/examples/webhooks/app.py @@ -5,6 +5,7 @@ import requests import hmac import hashlib +import uuid try: from credentials import * @@ -55,7 +56,7 @@ def verify_request(request): return digest == request.headers.get('X-Nylas-Signature') -# Setup ngrok settings to ensure everything works locally +# Setup ngrok settings to ensure everything works locally def initialize(): # Make sure ngrok is running try: @@ -69,7 +70,6 @@ def initialize(): if __name__ == '__main__': - import uuid initialize() app.secret_key = str(uuid.uuid4()) app.debug = False diff --git a/nylas/client/client.py b/nylas/client/client.py index 4135460a..36f2a6a8 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -5,16 +5,19 @@ from base64 import b64encode from six.moves.urllib.parse import urlencode from nylas._client_sdk_version import __VERSION__ -from .util import url_concat, generate_id -from .restful_model_collection import RestfulModelCollection -from .restful_models import (Calendar, Contact, Event, Message, Thread, File, - Account, APIAccount, SingletonAccount, Folder, - Label, Draft) -from .errors import (APIClientError, ConnectionError, NotAuthorizedError, - InvalidRequestError, NotFoundError, MethodNotSupportedError, - ServerError, ServiceUnavailableError, ConflictError, - SendingQuotaExceededError, ServerTimeoutError, - MessageRejectedError) +from nylas.client.util import url_concat, generate_id +from nylas.client.restful_model_collection import RestfulModelCollection +from nylas.client.restful_models import ( + Calendar, Contact, Event, Message, Thread, File, + Account, APIAccount, SingletonAccount, Folder, + Label, Draft +) +from nylas.client.errors import ( + APIClientError, ConnectionError, NotAuthorizedError, + InvalidRequestError, NotFoundError, MethodNotSupportedError, + ServerError, ServiceUnavailableError, ConflictError, + SendingQuotaExceededError, ServerTimeoutError, MessageRejectedError +) DEBUG = environ.get('NYLAS_CLIENT_DEBUG') API_SERVER = "https://api.nylas.com" diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 7245f450..4a989afd 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -1,8 +1,6 @@ -from .restful_model_collection import RestfulModelCollection -from .errors import FileUploadError +from nylas.client.restful_model_collection import RestfulModelCollection +from nylas.client.errors import FileUploadError from six import StringIO -import base64 -import json class NylasAPIObject(dict): diff --git a/nylas/client/util.py b/nylas/client/util.py index 531be942..355787c8 100644 --- a/nylas/client/util.py +++ b/nylas/client/util.py @@ -1,4 +1,3 @@ -import six from six.moves.urllib.parse import urlencode from uuid import uuid4 from struct import unpack diff --git a/tests/conftest.py b/tests/conftest.py index de46d45c..de399e51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ import pytest import responses from nylas import APIClient -from nylas.client.errors import * API_URL = 'http://localhost:2222' diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py index 17ddedb0..b78e9616 100644 --- a/tests/test_folder_labels.py +++ b/tests/test_folder_labels.py @@ -2,10 +2,8 @@ import re import pytest import responses -import httpretty from nylas import APIClient from nylas.client.restful_models import Label, Folder -from nylas.client.errors import * API_URL = 'http://localhost:2222' diff --git a/tests/test_send_error_handling.py b/tests/test_send_error_handling.py index 1683a6f9..1e470e3a 100644 --- a/tests/test_send_error_handling.py +++ b/tests/test_send_error_handling.py @@ -3,7 +3,9 @@ import pytest import responses from nylas import APIClient -from nylas.client.errors import * +from nylas.client.errors import ( + MessageRejectedError, SendingQuotaExceededError, ServiceUnavailableError, +) API_URL = 'http://localhost:2222' From f78d1aaab45ed804ca0abd7789acbf0a5f102c57 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:11:28 -0400 Subject: [PATCH 026/451] pytest, not py.test See http://blog.pytest.org/2016/whats-new-in-pytest-30/ --- setup.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 282cfff8..b5a85ced 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")] + user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] def initialize_options(self): TestCommand.initialize_options(self) diff --git a/tox.ini b/tox.ini index ec874cae..f8380a3e 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,6 @@ envlist = py27,pypy,py34 [testenv] commands = pip install -e . - py.test + pytest deps = -rrequirements-dev.txt From 7847fa3e10b8fa23bcbc6c8dcd062fa1393b1dcf Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:17:23 -0400 Subject: [PATCH 027/451] Use container-based Travis CI https://docs.travis-ci.com/user/migrating-from-legacy/ --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 13a85b19..63ee80fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: - "2.7" From 0374de9426fd981d3167105bf5f371d86fa8337c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:07:25 -0400 Subject: [PATCH 028/451] Turn API_URL into a fixture So that we don't have to import from conftest.py, which is not supposed to be importable. --- tests/conftest.py | 19 ++++++++++--------- tests/test_drafts.py | 15 +++++++-------- tests/test_events.py | 12 +++++------- tests/test_files.py | 7 +++---- tests/test_filter.py | 22 ++++++++++------------ tests/test_search.py | 9 ++++----- 6 files changed, 39 insertions(+), 45 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index de399e51..2eaa88dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,16 +4,19 @@ import responses from nylas import APIClient -API_URL = 'http://localhost:2222' + +@pytest.fixture +def api_url(): + return 'http://localhost:2222' @pytest.fixture -def api_client(): - return APIClient(None, None, None, API_URL) +def api_client(api_url): + return APIClient(None, None, None, api_url) @pytest.fixture -def mock_account(): +def mock_account(api_url): response_body = json.dumps([ { "account_id": "4dl0ni6vxomazo73r5ozdo16j", @@ -24,14 +27,14 @@ def mock_account(): "provider": "gmail" } ]) - responses.add(responses.GET, API_URL + '/n?limit=1&offset=0', + responses.add(responses.GET, api_url + '/n?limit=1&offset=0', content_type='application/json', status=200, body=response_body, match_querystring=True) @pytest.fixture -def mock_save_draft(): - save_endpoint = re.compile(API_URL + '/drafts/') +def mock_save_draft(api_url): + save_endpoint = re.compile(api_url + '/drafts/') response_body = json.dumps({ "id": "4dl0ni6vxomazo73r5oydo16k", "version": "4dw0ni6txomazo33r5ozdo16j" @@ -39,5 +42,3 @@ def mock_save_draft(): responses.add(responses.POST, save_endpoint, content_type='application/json', status=200, body=response_body, match_querystring=True) - - diff --git a/tests/test_drafts.py b/tests/test_drafts.py index ed9f85e9..97e7066c 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -1,12 +1,11 @@ import json import pytest import responses -from conftest import API_URL from nylas.client.errors import InvalidRequestError @pytest.fixture -def mock_draft_saved_response(): +def mock_draft_saved_response(api_url): response_body = json.dumps( { "bcc": [], @@ -36,13 +35,13 @@ def mock_draft_saved_response(): "version": 0 }) - responses.add(responses.POST, API_URL + '/drafts/', + responses.add(responses.POST, api_url + '/drafts/', content_type='application/json', status=200, body=response_body, match_querystring=True) @pytest.fixture -def mock_draft_updated_response(): +def mock_draft_updated_response(api_url): body = { "bcc": [], "body": "", @@ -71,18 +70,18 @@ def mock_draft_updated_response(): "version": 0 } - responses.add(responses.PUT, API_URL + '/drafts/2h111aefv8pzwzfykrn7hercj', + responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', status=200, body=json.dumps(body), match_querystring=True) body['subject'] = 'Update #2' - responses.add(responses.PUT, API_URL + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param', + responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param', content_type='application/json', status=200, body=json.dumps(body), match_querystring=True) @pytest.fixture -def mock_draft_sent_response(): +def mock_draft_sent_response(api_url): body = { "bcc": [], "body": "", @@ -121,7 +120,7 @@ def callback(request): return values.pop() responses.add_callback( - responses.POST, API_URL + '/send/', + responses.POST, api_url + '/send/', callback=callback, content_type='application/json') diff --git a/tests/test_events.py b/tests/test_events.py index 9e3cdfec..b7a85a5c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -3,11 +3,9 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError -url = API_URL + '/events/' body = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", @@ -31,21 +29,21 @@ @pytest.fixture -def mock_event_create_response(): +def mock_event_create_response(api_url): values = [Response(status=200, body=json.dumps(body)), Response(status=400, body='')] - httpretty.register_uri(httpretty.POST, API_URL + '/events/', responses=values) + httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) put_values = [Response(status=200, body=json.dumps({'title': 'loaded from JSON', 'ignored': 'ignored'}))] - httpretty.register_uri(httpretty.PUT, API_URL + '/events/cv4ei7syx10uvsxbs21ccsezf', + httpretty.register_uri(httpretty.PUT, api_url + '/events/cv4ei7syx10uvsxbs21ccsezf', responses=put_values) @pytest.fixture -def mock_event_create_notify_response(): - httpretty.register_uri(httpretty.POST, API_URL + '/events/?notify_participants=true&other_param=1', +def mock_event_create_notify_response(api_url): + httpretty.register_uri(httpretty.POST, api_url + '/events/?notify_participants=true&other_param=1', body=json.dumps(body), status=200) diff --git a/tests/test_files.py b/tests/test_files.py index 46f75db8..e024b26e 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -3,11 +3,10 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError, FileUploadError -def test_file_upload(api_client): +def test_file_upload(api_client, api_url): httpretty.enable() body = [{ "content_type": "text/plain", @@ -19,8 +18,8 @@ def test_file_upload(api_client): }] values = [Response(status=200, body=json.dumps(body))] - httpretty.register_uri(httpretty.POST, API_URL + '/files/', responses=values) - httpretty.register_uri(httpretty.GET, API_URL + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', + httpretty.register_uri(httpretty.POST, api_url + '/files/', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', body='test body') myfile = api_client.files.create() diff --git a/tests/test_filter.py b/tests/test_filter.py index 2fb9e244..042262ab 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -4,11 +4,9 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError -url = API_URL + '/events/' default_body = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", @@ -34,13 +32,13 @@ body2 = [default_body for i in range(1, 23)] -def test_no_filter(api_client): +def test_no_filter(api_client, api_url): httpretty.enable() # httpretty kind of sucks and strips & parameters from the URL values = [Response(status=200, body=json.dumps(body)), Response(status=200, body=json.dumps(body2))] - httpretty.register_uri(httpretty.GET, API_URL + '/events', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events', responses=values) events = api_client.events.all() assert len(events) == 72 @@ -49,11 +47,11 @@ def test_no_filter(api_client): httpretty.disable() -def test_two_filters(api_client): +def test_two_filters(api_client, api_url): httpretty.enable() values2 = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?param1=a¶m2=b', responses=values2) + httpretty.register_uri(httpretty.GET, api_url + '/events?param1=a¶m2=b', responses=values2) events = api_client.events.where(param1='a', param2='b').all() assert len(events) == 0 qs = httpretty.last_request().querystring @@ -61,11 +59,11 @@ def test_two_filters(api_client): assert qs['param2'][0] == 'b' httpretty.disable() -def test_no_offset(api_client): +def test_no_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas', responses=values) events = api_client.events.where({'in': 'Nylas'}).items() for event in events: pass @@ -74,11 +72,11 @@ def test_no_offset(api_client): assert qs['offset'][0] == '0' httpretty.disable() -def test_zero_offset(api_client): +def test_zero_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas&offset=0', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=0', responses=values) events = api_client.events.where({'in': 'Nylas', 'offset': 0}).items() for event in events: pass @@ -87,12 +85,12 @@ def test_zero_offset(api_client): assert qs['offset'][0] == '0' httpretty.disable() -def test_non_zero_offset(api_client): +def test_non_zero_offset(api_client, api_url): httpretty.enable() offset = random.randint(1,1000) values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas&offset=' + + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=' + str(offset), responses=values) events = api_client.events.where({'in': 'Nylas', 'offset': offset}).items() for event in events: diff --git a/tests/test_search.py b/tests/test_search.py index 9c813306..6e84e2b0 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,11 +1,10 @@ import json import pytest import responses -from conftest import API_URL from nylas.client.errors import InvalidRequestError @pytest.fixture -def mock_thread_search_response(): +def mock_thread_search_response(api_url): response_body = json.dumps( [ { @@ -45,13 +44,13 @@ def mock_thread_search_response(): ]) responses.add(responses.GET, - API_URL + '/threads/search?q=Helena', + 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(): +def mock_message_search_response(api_url): response_body = json.dumps( [ { @@ -125,7 +124,7 @@ def mock_message_search_response(): ]) responses.add(responses.GET, - API_URL + '/messages/search?q=Pinot', + api_url + '/messages/search?q=Pinot', body=response_body, status=200, content_type='application/json', match_querystring=True) From 5d5a871b651790fe6cc106ce85114db24b648c26 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 08:52:34 -0400 Subject: [PATCH 029/451] Change print statement to print() function --- README.md | 34 ++++++++++++++++----------------- examples/lib/random_words.py | 7 ++++--- examples/most_talked_to.py | 4 ++-- examples/nylas-connect/app.py | 9 +++++---- examples/server.py | 4 +++- examples/upload_and_download.py | 4 ++-- examples/upload_files_in_dir.py | 4 ++-- examples/webhooks/app.py | 9 +++++---- nylas/client/client.py | 1 + 9 files changed, 41 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9798fe4c..996a4fe2 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ use the `revoke_token` method on APIClient client = APIClient(APP_ID, APP_SECRET, token) # Print out the email address and provider (Gmail, Exchange) -print client.account.email_address -print client.account.provider +print(client.account.email_address) +print(client.account.provider) ``` @@ -104,19 +104,19 @@ thread = client.threads.find('ac123acd123ef123') # List all threads tagged `inbox` # (paginating 50 at a time until no more are returned.) for thread in client.threads.items(): - print thread.subject + print(thread.subject) # List the 5 most recent unread threads for thread in client.threads.where(unread=True, limit=5): - print thread.subject + print(thread.subject) # List starred threads for thread in client.threads.where(starred=True): - print thread.subject + print(thread.subject) # List all threads with 'ben@nylas.com' for thread in client.threads.where(any_email='ben@nylas.com').items(): - print thread.subject + print(thread.subject) ``` ### Searching Messages and Threads @@ -137,7 +137,7 @@ messages = client.messages.search("nylas") ```python # List thread participants for participant in thread.participants: - print participant["email"] + print(participant["email"]) # Mark as read thread.mark_as_read() @@ -170,10 +170,10 @@ message.update_folder(trash_id) # List messages for message in thread.messages.items(): - print message.subject + print(message.subject) # Get the raw contents of a message -print message.raw +print(message.raw) ``` To get the [expanded message view](https://www.nylas.com/docs/platform#expanded_message_view) that includes convenient header information, include `view='expanded'` in the where clause @@ -191,7 +191,7 @@ The Folders and Labels API replaces the now deprecated Tags API. For Gmail accou ```python # List labels for label in client.labels: - print label.id, label.display_name + print(label.id, label.display_name) # Create a label label = client.labels.create() @@ -219,7 +219,7 @@ Files can be uploaded via two interfaces. One is providing data directly, anothe ```python # List files for file in client.files: - print file.filename + print(file.filename) # Create a new file with the stream interface f = open('test.py', 'r') @@ -259,15 +259,15 @@ draft.attach(myfile) try: draft.send() except nylas.client.errors.ConnectionError as e: - print "Unable to connect to the SMTP server." + print("Unable to connect to the SMTP server.") except nylas.client.errors.MessageRejectedError as e: - print "Message got rejected by the SMTP server!" - print e.message + print("Message got rejected by the SMTP server!") + print(e.message) # Sometimes the API gives us the exact error message # returned by the server. Display it since it can be # helpful to know exactly why our message got rejected: - print e.server_error + print(e.server_error) # Delete a draft draft = client.drafts.create() @@ -352,7 +352,7 @@ It's possible to query the status of all the user accounts registered to an app ```python accounts = client.accounts -print [(acc.sync_status, acc.account_id, acc.trial, acc.trial_expires) for acc in accounts.all()] +print([(acc.sync_status, acc.account_id, acc.trial, acc.trial_expires) for acc in accounts.all()]) ``` ## Open-Source Sync Engine @@ -369,7 +369,7 @@ account_id = client.accounts.first().id # Display the contents of the first message for the first account client = APIClient(None, None, account_id, 'http://localhost:5555/') -print client.messages.first().body +print(client.messages.first().body) ``` diff --git a/examples/lib/random_words.py b/examples/lib/random_words.py index 7f69888a..07174dda 100755 --- a/examples/lib/random_words.py +++ b/examples/lib/random_words.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import print_function import random import json import sys @@ -11,8 +12,8 @@ def get_words(): with open(DICT_FILE, 'r') as f: words.extend(f.read().split('\n')) except IOError: - print json.dumps({'error': "couldn't open dictionary file", - 'filename': DICT_FILE}) + print(json.dumps({'error': "couldn't open dictionary file", + 'filename': DICT_FILE})) sys.exit(1) return words @@ -68,4 +69,4 @@ def random_words(count=int(random.uniform(1,500)), sig='me'): if __name__ == '__main__': - print random_words() + print(random_words()) diff --git a/examples/most_talked_to.py b/examples/most_talked_to.py index bf50bbb6..378e86e6 100644 --- a/examples/most_talked_to.py +++ b/examples/most_talked_to.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +from __future__ import print_function from operator import itemgetter from nylas import APIClient @@ -18,4 +18,4 @@ most_chatted = sorted(counts.iteritems(), key=itemgetter(1)) for i in most_chatted: - print i + print(i) diff --git a/examples/nylas-connect/app.py b/examples/nylas-connect/app.py index ca81ae03..12c91cf3 100755 --- a/examples/nylas-connect/app.py +++ b/examples/nylas-connect/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import print_function import json import sys import os @@ -175,12 +176,12 @@ def nylas_token(data): def initialize(): global REDIRECT_URI REDIRECT_URI = "{}/oauth2callback".format("http://lvh.me:1234") - print REDIRECT_URI + print(REDIRECT_URI) s = raw_input("Have you added the url above as an authorized callback " "in Google's Developer console? y/n ") if s != "y": - print "You need to set that up first!" - print "See https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide for more information" + print("You need to set that up first!") + print("See https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide for more information") sys.exit(-1) @@ -189,5 +190,5 @@ def initialize(): initialize() app.secret_key = str(uuid.uuid4()) app.debug = False - print "Visit http://localhost:1234 in your browser" + print("Visit http://localhost:1234 in your browser") app.run(port=1234) diff --git a/examples/server.py b/examples/server.py index 493a6e96..2781c921 100755 --- a/examples/server.py +++ b/examples/server.py @@ -33,6 +33,8 @@ # 6. In the browser, visit http://localhost:8888/ # +from __future__ import print_function + import time from flask import Flask, url_for, session, request, redirect, Response @@ -60,7 +62,7 @@ def index(): # Get the latest message from namespace zero. message = client.messages.first() if not message: # A new account takes a little time to sync - print "No messages yet. Checking again in 2 seconds." + print("No messages yet. Checking again in 2 seconds.") time.sleep(2) except Exception as e: print(e.message) diff --git a/examples/upload_and_download.py b/examples/upload_and_download.py index 7857dfed..80ef3f13 100755 --- a/examples/upload_and_download.py +++ b/examples/upload_and_download.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +from __future__ import print_function import time from nylas import APIClient from nylas.util import generate_id @@ -36,4 +36,4 @@ m = th.messages[0] -print m.attachments[0].download() +print(m.attachments[0].download()) diff --git a/examples/upload_files_in_dir.py b/examples/upload_files_in_dir.py index 5df3f3fd..8f49ee86 100755 --- a/examples/upload_files_in_dir.py +++ b/examples/upload_files_in_dir.py @@ -1,5 +1,5 @@ #!/usr/bin/python - +from __future__ import print_function import os import time from nylas import APIClient @@ -32,4 +32,4 @@ time.sleep(0.5) th = client.threads.where({'in': 'Sent', 'subject': subject}).first() -print th.messages[0].attachments[0].download() +print(th.messages[0].attachments[0].download()) diff --git a/examples/webhooks/app.py b/examples/webhooks/app.py index 1e56f337..e0bdb302 100755 --- a/examples/webhooks/app.py +++ b/examples/webhooks/app.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import print_function import sys import json import flask @@ -37,8 +38,8 @@ def index(): # Print some of the information Nylas sent us. This is where you # would normally process the webhook notification and do things like # fetch relevant message ids, update your database, etc. - print "{} at {} with id {}".format(delta['type'], delta['date'], - delta['object_data']['id']) + print("{} at {} with id {}".format(delta['type'], delta['date'], + delta['object_data']['id'])) # Don't forget to let Nylas know that everything was pretty ok. return "Success", 200 @@ -62,7 +63,7 @@ def initialize(): try: resp = requests.get('http://localhost:4040/api/tunnels').json() except requests.exceptions.ConnectionError: - print "It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'" + print("It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'") sys.exit(-1) global WEBHOOK_URI @@ -73,5 +74,5 @@ def initialize(): initialize() app.secret_key = str(uuid.uuid4()) app.debug = False - print "{}\nAdd the above url to the webhooks page at https://developer.nylas.com".format(WEBHOOK_URI) + print("{}\nAdd the above url to the webhooks page at https://developer.nylas.com".format(WEBHOOK_URI)) app.run(port=1234) diff --git a/nylas/client/client.py b/nylas/client/client.py index 36f2a6a8..e5807a7c 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -1,3 +1,4 @@ +from __future__ import print_function import sys import requests import json From bd804f87711ecb835082fa25fcadc7a312d99463 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:13:30 -0400 Subject: [PATCH 030/451] Travis CI: test on Python 3.6 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 63ee80fa..c075681e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ sudo: false language: python python: - "2.7" + - "3.6" install: - "python setup.py install" - "travis_retry pip install codecov" From c79cc46ec1e334ce0eb2cd5710f08c8d77209b86 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:21:22 -0400 Subject: [PATCH 031/451] Pass list of arguments to pytest.main() Passing a string to pytest.main() is deprecated --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b5a85ced..bfbf694a 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ class PyTest(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) - self.pytest_args = '--cov --junitxml ./tests/output tests/' + self.pytest_args = ['--cov', '--junitxml', './tests/output', 'tests/'] def finalize_options(self): TestCommand.finalize_options(self) From bb1075eec930b2b4a995b6fecdc6345c24911d45 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 3 Jul 2017 09:07:25 -0400 Subject: [PATCH 032/451] Turn API_URL into a fixture So that we don't have to import from conftest.py, which is not supposed to be importable. --- tests/conftest.py | 19 ++++++++++--------- tests/test_drafts.py | 15 +++++++-------- tests/test_events.py | 12 +++++------- tests/test_files.py | 7 +++---- tests/test_filter.py | 22 ++++++++++------------ tests/test_search.py | 9 ++++----- 6 files changed, 39 insertions(+), 45 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index de399e51..2eaa88dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,16 +4,19 @@ import responses from nylas import APIClient -API_URL = 'http://localhost:2222' + +@pytest.fixture +def api_url(): + return 'http://localhost:2222' @pytest.fixture -def api_client(): - return APIClient(None, None, None, API_URL) +def api_client(api_url): + return APIClient(None, None, None, api_url) @pytest.fixture -def mock_account(): +def mock_account(api_url): response_body = json.dumps([ { "account_id": "4dl0ni6vxomazo73r5ozdo16j", @@ -24,14 +27,14 @@ def mock_account(): "provider": "gmail" } ]) - responses.add(responses.GET, API_URL + '/n?limit=1&offset=0', + responses.add(responses.GET, api_url + '/n?limit=1&offset=0', content_type='application/json', status=200, body=response_body, match_querystring=True) @pytest.fixture -def mock_save_draft(): - save_endpoint = re.compile(API_URL + '/drafts/') +def mock_save_draft(api_url): + save_endpoint = re.compile(api_url + '/drafts/') response_body = json.dumps({ "id": "4dl0ni6vxomazo73r5oydo16k", "version": "4dw0ni6txomazo33r5ozdo16j" @@ -39,5 +42,3 @@ def mock_save_draft(): responses.add(responses.POST, save_endpoint, content_type='application/json', status=200, body=response_body, match_querystring=True) - - diff --git a/tests/test_drafts.py b/tests/test_drafts.py index ed9f85e9..97e7066c 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -1,12 +1,11 @@ import json import pytest import responses -from conftest import API_URL from nylas.client.errors import InvalidRequestError @pytest.fixture -def mock_draft_saved_response(): +def mock_draft_saved_response(api_url): response_body = json.dumps( { "bcc": [], @@ -36,13 +35,13 @@ def mock_draft_saved_response(): "version": 0 }) - responses.add(responses.POST, API_URL + '/drafts/', + responses.add(responses.POST, api_url + '/drafts/', content_type='application/json', status=200, body=response_body, match_querystring=True) @pytest.fixture -def mock_draft_updated_response(): +def mock_draft_updated_response(api_url): body = { "bcc": [], "body": "", @@ -71,18 +70,18 @@ def mock_draft_updated_response(): "version": 0 } - responses.add(responses.PUT, API_URL + '/drafts/2h111aefv8pzwzfykrn7hercj', + responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', status=200, body=json.dumps(body), match_querystring=True) body['subject'] = 'Update #2' - responses.add(responses.PUT, API_URL + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param', + responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param', content_type='application/json', status=200, body=json.dumps(body), match_querystring=True) @pytest.fixture -def mock_draft_sent_response(): +def mock_draft_sent_response(api_url): body = { "bcc": [], "body": "", @@ -121,7 +120,7 @@ def callback(request): return values.pop() responses.add_callback( - responses.POST, API_URL + '/send/', + responses.POST, api_url + '/send/', callback=callback, content_type='application/json') diff --git a/tests/test_events.py b/tests/test_events.py index 9e3cdfec..b7a85a5c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -3,11 +3,9 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError -url = API_URL + '/events/' body = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", @@ -31,21 +29,21 @@ @pytest.fixture -def mock_event_create_response(): +def mock_event_create_response(api_url): values = [Response(status=200, body=json.dumps(body)), Response(status=400, body='')] - httpretty.register_uri(httpretty.POST, API_URL + '/events/', responses=values) + httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) put_values = [Response(status=200, body=json.dumps({'title': 'loaded from JSON', 'ignored': 'ignored'}))] - httpretty.register_uri(httpretty.PUT, API_URL + '/events/cv4ei7syx10uvsxbs21ccsezf', + httpretty.register_uri(httpretty.PUT, api_url + '/events/cv4ei7syx10uvsxbs21ccsezf', responses=put_values) @pytest.fixture -def mock_event_create_notify_response(): - httpretty.register_uri(httpretty.POST, API_URL + '/events/?notify_participants=true&other_param=1', +def mock_event_create_notify_response(api_url): + httpretty.register_uri(httpretty.POST, api_url + '/events/?notify_participants=true&other_param=1', body=json.dumps(body), status=200) diff --git a/tests/test_files.py b/tests/test_files.py index 46f75db8..e024b26e 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -3,11 +3,10 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError, FileUploadError -def test_file_upload(api_client): +def test_file_upload(api_client, api_url): httpretty.enable() body = [{ "content_type": "text/plain", @@ -19,8 +18,8 @@ def test_file_upload(api_client): }] values = [Response(status=200, body=json.dumps(body))] - httpretty.register_uri(httpretty.POST, API_URL + '/files/', responses=values) - httpretty.register_uri(httpretty.GET, API_URL + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', + httpretty.register_uri(httpretty.POST, api_url + '/files/', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', body='test body') myfile = api_client.files.create() diff --git a/tests/test_filter.py b/tests/test_filter.py index 2fb9e244..042262ab 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -4,11 +4,9 @@ import responses import httpretty from httpretty import Response -from conftest import API_URL from nylas.client.errors import InvalidRequestError -url = API_URL + '/events/' default_body = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", @@ -34,13 +32,13 @@ body2 = [default_body for i in range(1, 23)] -def test_no_filter(api_client): +def test_no_filter(api_client, api_url): httpretty.enable() # httpretty kind of sucks and strips & parameters from the URL values = [Response(status=200, body=json.dumps(body)), Response(status=200, body=json.dumps(body2))] - httpretty.register_uri(httpretty.GET, API_URL + '/events', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events', responses=values) events = api_client.events.all() assert len(events) == 72 @@ -49,11 +47,11 @@ def test_no_filter(api_client): httpretty.disable() -def test_two_filters(api_client): +def test_two_filters(api_client, api_url): httpretty.enable() values2 = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?param1=a¶m2=b', responses=values2) + httpretty.register_uri(httpretty.GET, api_url + '/events?param1=a¶m2=b', responses=values2) events = api_client.events.where(param1='a', param2='b').all() assert len(events) == 0 qs = httpretty.last_request().querystring @@ -61,11 +59,11 @@ def test_two_filters(api_client): assert qs['param2'][0] == 'b' httpretty.disable() -def test_no_offset(api_client): +def test_no_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas', responses=values) events = api_client.events.where({'in': 'Nylas'}).items() for event in events: pass @@ -74,11 +72,11 @@ def test_no_offset(api_client): assert qs['offset'][0] == '0' httpretty.disable() -def test_zero_offset(api_client): +def test_zero_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas&offset=0', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=0', responses=values) events = api_client.events.where({'in': 'Nylas', 'offset': 0}).items() for event in events: pass @@ -87,12 +85,12 @@ def test_zero_offset(api_client): assert qs['offset'][0] == '0' httpretty.disable() -def test_non_zero_offset(api_client): +def test_non_zero_offset(api_client, api_url): httpretty.enable() offset = random.randint(1,1000) values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, API_URL + '/events?in=Nylas&offset=' + + httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=' + str(offset), responses=values) events = api_client.events.where({'in': 'Nylas', 'offset': offset}).items() for event in events: diff --git a/tests/test_search.py b/tests/test_search.py index 9c813306..6e84e2b0 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,11 +1,10 @@ import json import pytest import responses -from conftest import API_URL from nylas.client.errors import InvalidRequestError @pytest.fixture -def mock_thread_search_response(): +def mock_thread_search_response(api_url): response_body = json.dumps( [ { @@ -45,13 +44,13 @@ def mock_thread_search_response(): ]) responses.add(responses.GET, - API_URL + '/threads/search?q=Helena', + 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(): +def mock_message_search_response(api_url): response_body = json.dumps( [ { @@ -125,7 +124,7 @@ def mock_message_search_response(): ]) responses.add(responses.GET, - API_URL + '/messages/search?q=Pinot', + api_url + '/messages/search?q=Pinot', body=response_body, status=200, content_type='application/json', match_querystring=True) From 5437d433bcf4bacb99bb5565d1477b7a8474d6fc Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 10:07:43 -0400 Subject: [PATCH 033/451] pylint --- .travis.yml | 2 +- nylas/client/client.py | 67 ++-- nylas/client/errors.py | 4 +- nylas/client/restful_model_collection.py | 18 +- nylas/client/restful_models.py | 62 ++-- nylas/client/util.py | 4 +- pylintrc | 445 +++++++++++++++++++++++ setup.py | 23 +- tests/conftest.py | 1 + tests/test_drafts.py | 212 ++++++----- tests/test_events.py | 58 +-- tests/test_files.py | 3 +- tests/test_filter.py | 87 +++-- tests/test_folder_labels.py | 26 +- tests/test_search.py | 257 +++++++------ tests/test_send_error_handling.py | 19 +- 16 files changed, 907 insertions(+), 381 deletions(-) create mode 100644 pylintrc diff --git a/.travis.yml b/.travis.yml index c075681e..4e9b8c27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,6 @@ python: install: - "python setup.py install" - "travis_retry pip install codecov" -script: "python setup.py test" +script: "python setup.py test -a --pylint" after_success: - "codecov" diff --git a/nylas/client/client.py b/nylas/client/client.py index e5807a7c..52f0028f 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -1,9 +1,9 @@ from __future__ import print_function import sys -import requests import json from os import environ from base64 import b64encode +import requests from six.moves.urllib.parse import urlencode from nylas._client_sdk_version import __VERSION__ from nylas.client.util import url_concat, generate_id @@ -73,10 +73,10 @@ def _validate(response): data=data, message="Unknown status code.") -def nylas_excepted(f): +def nylas_excepted(func): def caught(*args, **kwargs): try: - return f(*args, **kwargs) + return func(*args, **kwargs) except requests.exceptions.ConnectionError: server = args[0].api_server raise ConnectionError(url=server) @@ -89,8 +89,7 @@ class APIClient(json.JSONEncoder): def __init__(self, app_id=environ.get('NYLAS_APP_ID'), app_secret=environ.get('NYLAS_APP_SECRET'), access_token=environ.get('NYLAS_ACCESS_TOKEN'), - api_server=API_SERVER, - auth_server=None): + api_server=API_SERVER): if "://" not in api_server: raise Exception("When overriding the Nylas API server address, you" " must include https://") @@ -110,7 +109,9 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), revision) self.session.headers = {'X-Nylas-API-Wrapper': 'python', '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 app_secret. Set up a specific session for this. @@ -118,10 +119,12 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), if app_secret is not None: b64_app_secret = b64encode(app_secret + ':') - self.admin_session.headers = {'Authorization': 'Basic ' + - b64_app_secret, - 'X-Nylas-API-Wrapper': 'python', - 'User-Agent': version_header} + self.admin_session.headers = { + 'Authorization': 'Basic {secret}'.format(secret=b64_app_secret), + 'X-Nylas-API-Wrapper': 'python', + 'User-Agent': version_header, + } + super(APIClient, self).__init__() @property def access_token(self): @@ -131,8 +134,8 @@ def access_token(self): def access_token(self, value): self._access_token = value if value: - self.session.headers.update({'Authorization': 'Bearer ' + - value}) + authorization = 'Bearer {token}'.format(token=value) + self.session.headers['Authorization'] = authorization else: if 'Authorization' in self.session.headers: del self.session.headers['Authorization'] @@ -183,8 +186,7 @@ def account(self): def accounts(self): if self.is_opensource_api(): return RestfulModelCollection(APIAccount, self) - else: - return RestfulModelCollection(Account, self) + return RestfulModelCollection(Account, self) @property def threads(self): @@ -232,8 +234,7 @@ def _get_http_session(self, api_root): # instead of the secret_token if api_root == 'a': return self.admin_session - else: - return self.session + return self.session @nylas_excepted def _get_resources(self, cls, extra=None, **filters): @@ -241,20 +242,27 @@ def _get_resources(self, cls, extra=None, **filters): # of the old accounts API. postfix = "/{}".format(extra) if extra else '' if cls.api_root != 'a': - url = "{}/{}{}".format(self.api_server, cls.collection_name, postfix) + url = "{}/{}{}".format( + self.api_server, + cls.collection_name, + postfix + ) else: - url = "{}/a/{}/{}{}".format(self.api_server, self.app_id, - cls.collection_name, postfix) + url = "{}/a/{}/{}{}".format( + self.api_server, + self.app_id, + cls.collection_name, + postfix + ) url = url_concat(url, filters) response = self._get_http_session(cls.api_root).get(url) results = _validate(response).json() - return list( - filter( - lambda x: x is not None, - map(lambda x: cls.create(self, **x), results) - ) - ) + return [ + cls.create(self, **x) + for x in results + if x is not None + ] @nylas_excepted def _get_resource_raw(self, cls, id, extra=None, @@ -325,7 +333,7 @@ def _create_resources(self, cls, data): response = session.post(url, data=data, headers=headers) results = _validate(response).json() - return list(map(lambda x: cls.create(self, **x), results)) + return [cls.create(self, **x) for x in results] @nylas_excepted def _delete_resource(self, cls, id, data=None, **kwargs): @@ -364,8 +372,13 @@ def _call_resource_method(self, cls, id, method_name, data): url = "{}/{}/{}/{}".format(self.api_server, name, id, method_name) else: # Management method. - url = "{}/a/{}/{}/{}/{}".format(self.api_server, self.app_id, - cls.collection_name, id, method_name) + url = "{}/a/{}/{}/{}/{}".format( + self.api_server, + self.app_id, + cls.collection_name, + id, + method_name, + ) session = self._get_http_session(cls.api_root) diff --git a/nylas/client/errors.py b/nylas/client/errors.py index 026ce175..d5641547 100644 --- a/nylas/client/errors.py +++ b/nylas/client/errors.py @@ -9,8 +9,8 @@ def __init__(self, **kwargs): Exception.__init__(self, '') self.attrs = kwargs.keys() - for k, v in kwargs.items(): - setattr(self, k, v) + for key, value in kwargs.items(): + setattr(self, key, value) def as_dict(self): resp = {} diff --git a/nylas/client/restful_model_collection.py b/nylas/client/restful_model_collection.py index 6a4df0e5..acff785c 100644 --- a/nylas/client/restful_model_collection.py +++ b/nylas/client/restful_model_collection.py @@ -3,9 +3,10 @@ CHUNK_SIZE = 50 class RestfulModelCollection(object): - def __init__(self, cls, api, filter={}, offset=0, + def __init__(self, cls, api, filter=None, offset=0, **filters): - filters.update(filter) + if filter: + filters.update(filter) from nylas.client import APIClient if not isinstance(api, APIClient): raise Exception("Provided api was not an APIClient.") @@ -36,14 +37,14 @@ def items(self): def first(self): results = self._get_model_collection(0, 1) - if len(results): + if results: return results[0] return None def all(self, limit=float('infinity')): return self._range(self.filters['offset'], limit) - def where(self, filter={}, **filters): + 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 @@ -55,7 +56,8 @@ def where(self, filter={}, **filters): filters[keyword] = filters.get(escaped_keyword) del filters[escaped_keyword] - filters.update(filter) + if filter: + filters.update(filter) filters.setdefault('offset', 0) collection = copy(self) collection.filters = filters @@ -70,10 +72,10 @@ def create(self, **kwargs): def delete(self, id, data=None, **kwargs): return self.api._delete_resource(self.model_class, id, data=data, **kwargs) - def search(self, q): - from .restful_models import (Message, Thread) + 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 } + 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") diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 4a989afd..701c79d8 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -2,6 +2,8 @@ from nylas.client.errors import FileUploadError from six import StringIO +# pylint: disable=attribute-defined-outside-init + class NylasAPIObject(dict): attrs = [] @@ -15,6 +17,7 @@ def __init__(self, cls, api): self.id = None self.cls = cls self.api = api + super(NylasAPIObject, self).__init__() __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @@ -31,7 +34,7 @@ def create(cls, api, **kwargs): # We need a special case for accounts because the /accounts API # is different between the open source and hosted API. return - obj = cls(api) + 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 @@ -100,8 +103,7 @@ def labels(self): if self._labels: return [Label.create(self.api, **l) for l in self._labels] - else: - return [] + return [] def update_folder(self, folder_id): update = {'folder': folder_id} @@ -111,7 +113,8 @@ def update_folder(self, folder_id): setattr(self, attr, getattr(new_obj, attr)) return self.folder - def update_labels(self, label_ids=[]): + 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: @@ -119,7 +122,8 @@ def update_labels(self, label_ids=[]): setattr(self, attr, getattr(new_obj, attr)) return self.labels - def add_labels(self, label_ids=[]): + 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) @@ -127,7 +131,8 @@ def add_labels(self, label_ids=[]): def add_label(self, label_id): return self.add_labels([label_id]) - def remove_labels(self, label_ids=[]): + 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) @@ -221,16 +226,14 @@ def folders(self): if self._folders: return [Folder.create(self.api, **f) for f in self._folders] - else: - return [] + return [] @property def labels(self): if self._labels: return [Label.create(self.api, **l) for l in self._labels] - else: - return [] + return [] def update_folder(self, folder_id): update = {'folder': folder_id} @@ -240,7 +243,8 @@ def update_folder(self, folder_id): setattr(self, attr, getattr(new_obj, attr)) return self.folder - def update_labels(self, label_ids=[]): + 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: @@ -248,7 +252,8 @@ def update_labels(self, label_ids=[]): setattr(self, attr, getattr(new_obj, attr)) return self.labels - def add_labels(self, label_ids=[]): + 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) @@ -256,7 +261,8 @@ def add_labels(self, label_ids=[]): def add_label(self, label_id): return self.add_labels([label_id]) - def remove_labels(self, label_ids=[]): + 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) @@ -288,18 +294,18 @@ def unstar(self): self.starred = False def create_reply(self): - d = self.drafts.create() - d.thread_id = self.id - d.subject = self.subject - return d + 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): - NylasAPIObject.__init__(self, Send, api) + def __init__(self, api): # pylint: disable=super-init-not-called + NylasAPIObject.__init__(self, Send, api) # pylint: disable=non-parent-init-called class Draft(Message): @@ -309,9 +315,9 @@ class Draft(Message): "reply_to", "starred", "snippet", "tracking"] collection_name = 'drafts' - def __init__(self, api, thread_id=None): + def __init__(self, api, thread_id=None): # pylint: disable=unused-argument Message.__init__(self, api) - NylasAPIObject.__init__(self, Thread, api) + NylasAPIObject.__init__(self, Thread, api) # pylint: disable=non-parent-init-called self.file_ids = [] def attach(self, file): @@ -346,15 +352,17 @@ class File(NylasAPIObject): "account_id", "object", "size", "message_ids", ] collection_name = 'files' - def save(self): + def save(self): # pylint: disable=arguments-differ if hasattr(self, 'stream') and self.stream is not None: data = {self.filename: self.stream} elif hasattr(self, 'data') and self.data is not None: data = {self.filename: StringIO(self.data)} else: - raise FileUploadError(message=("File object not properly " - "formatted, must provide " - "either a stream or data.")) + message = ( + "File object not properly formatted, " + "must provide either a stream or data." + ) + raise FileUploadError(message=message) new_obj = self.api._create_resources(File, data) new_obj = new_obj[0] @@ -364,8 +372,8 @@ def save(self): def download(self): if not self.id: - raise FileUploadError(message=("Can't download a file that " - "hasn't been uploaded.")) + message = "Can't download a file that hasn't been uploaded." + raise FileUploadError(message=message) return self.api._get_resource_data(File, self.id, extra='download') diff --git a/nylas/client/util.py b/nylas/client/util.py index 355787c8..357637cd 100644 --- a/nylas/client/util.py +++ b/nylas/client/util.py @@ -1,6 +1,6 @@ -from six.moves.urllib.parse import urlencode from uuid import uuid4 from struct import unpack +from six.moves.urllib.parse import urlencode # From tornado.httputil @@ -33,7 +33,7 @@ def url_concat(url, args, fragments=None): def generate_id(): - a, b = unpack('>QQ', uuid4().bytes) + a, b = unpack('>QQ', uuid4().bytes) # pylint: disable=invalid-name num = a << 64 | b alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..20eed343 --- /dev/null +++ b/pylintrc @@ -0,0 +1,445 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=examples + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable= + print-statement, parameter-unpacking, unpacking-in-except, old-raise-syntax, + backtick, long-suffix, old-ne-operator, old-octal-literal, + import-star-module-level, raw-checker-failed, bad-inline-option, + locally-disabled, locally-enabled, file-ignored, suppressed-message, + useless-suppression, deprecated-pragma, apply-builtin, basestring-builtin, + buffer-builtin, cmp-builtin, coerce-builtin, execfile-builtin, + file-builtin, long-builtin, raw_input-builtin, reduce-builtin, + standarderror-builtin, unicode-builtin, xrange-builtin, coerce-method, + delslice-method, getslice-method, setslice-method, no-absolute-import, + old-division, dict-iter-method, dict-view-method, next-method-called, + metaclass-assignment, indexing-exception, raising-string, reload-builtin, + oct-method, hex-method, nonzero-method, cmp-method, input-builtin, + round-builtin, intern-builtin, unichr-builtin, map-builtin-not-iterating, + zip-builtin-not-iterating, range-builtin-not-iterating, + filter-builtin-not-iterating, using-cmp-argument, eq-without-hash, + div-method, idiv-method, rdiv-method, exception-message-attribute, + invalid-str-codec, sys-max-int, bad-python3-import, + deprecated-string-function, deprecated-str-translate-call, missing-docstring, + fixme, import-error, redefined-builtin, protected-access, + cyclic-import, duplicate-code + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[BASIC] + +# Naming hint for argument names +argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct argument names +argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for attribute names +attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct attribute names +attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming hint for function names +function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct function names +function-rgx=(([a-z][a-z0-9_]{2,50})|(_[a-z0-9_]*))$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,id + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for method names +method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct method names +method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming hint for variable names +variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,responses + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=6 + +# Maximum number of attributes for a class (see R0902). +max-attributes=12 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/setup.py b/setup.py index bfbf694a..d5be36f3 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,15 @@ import os import sys -sys.path.append('nylas/') - +import re from setuptools import setup, find_packages from setuptools.command.test import test as TestCommand -from _client_sdk_version import __VERSION__ + + +VERSION = '' +with open('nylas/_client_sdk_version.py', 'r') as fd: + VERSION = re.search(r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', + fd.read(), re.MULTILINE).group(1) + class PyTest(TestCommand): @@ -12,12 +17,12 @@ class PyTest(TestCommand): def initialize_options(self): TestCommand.initialize_options(self) - self.pytest_args = ['--cov', '--junitxml', './tests/output', 'tests/'] + self.pytest_args = ['--cov', '--junitxml', './tests/output', 'tests/'] # pylint: disable=attribute-defined-outside-init def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] - self.test_suite = True + self.test_suite = True # pylint: disable=attribute-defined-outside-init def run_tests(self): # import here, cause outside the eggs aren't loaded @@ -38,12 +43,12 @@ def main(): else: type_ = sys.argv[2] os.system('bumpversion --current-version {} {}' - .format(__VERSION__, type_)) + .format(VERSION, type_)) sys.exit() setup( name="nylas", - version=__VERSION__, + version=VERSION, packages=find_packages(), install_requires=[ @@ -56,7 +61,9 @@ def main(): "pyasn1", ], dependency_links=[], - tests_require=["pytest", "pytest-cov", "responses", "httpretty"], + tests_require=[ + "pytest", "pytest-cov", "pytest-pylint", "responses", "httpretty" + ], cmdclass={'test': PyTest}, author="Nylas Team", author_email="support@nylas.com", diff --git a/tests/conftest.py b/tests/conftest.py index 2eaa88dd..12fc6cac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import responses from nylas import APIClient +# pylint: disable=redefined-outer-name @pytest.fixture def api_url(): diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 97e7066c..aa15871f 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -6,109 +6,124 @@ @pytest.fixture def mock_draft_saved_response(api_url): - response_body = json.dumps( - { - "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 - }) - - responses.add(responses.POST, api_url + '/drafts/', - content_type='application/json', status=200, - body=response_body, match_querystring=True) + response_body = json.dumps({ + "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 + }) + + responses.add( + responses.POST, + api_url + '/drafts/', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) @pytest.fixture def mock_draft_updated_response(api_url): body = { - "bcc": [], - "body": "", - "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": "Stay polish, stay hungary", - "thread_id": "clm33kapdxkposgltof845v9s", - "to": [ - { - "email": "helena@nylas.com", - "name": "Helena Handbasket" - } - ], - "unread": False, - "version": 0 - } - - responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', - content_type='application/json', status=200, - body=json.dumps(body), match_querystring=True) + "bcc": [], + "body": "", + "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": "Stay polish, stay hungary", + "thread_id": "clm33kapdxkposgltof845v9s", + "to": [ + { + "email": "helena@nylas.com", + "name": "Helena Handbasket" + } + ], + "unread": False, + "version": 0 + } + + responses.add( + responses.PUT, + api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', + content_type='application/json', + status=200, + body=json.dumps(body), + match_querystring=True + ) body['subject'] = 'Update #2' - responses.add(responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param', - content_type='application/json', status=200, - body=json.dumps(body), match_querystring=True) + url = api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param' + responses.add( + responses.PUT, + url, + content_type='application/json', + status=200, + body=json.dumps(body), + match_querystring=True + ) @pytest.fixture def mock_draft_sent_response(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 - } + "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))] @@ -120,14 +135,19 @@ def callback(request): return values.pop() responses.add_callback( - responses.POST, api_url + '/send/', - callback=callback, - content_type='application/json') + responses.POST, + api_url + '/send/', + callback=callback, + content_type='application/json' + ) @responses.activate -def test_save_send_draft(api_client, mock_draft_saved_response, - mock_draft_updated_response, mock_draft_sent_response): +@pytest.mark.usefixtures( + "mock_draft_saved_response", "mock_draft_updated_response", + "mock_draft_sent_response" +) +def test_save_send_draft(api_client): draft = api_client.drafts.create() draft.to = [{'name': 'My Friend', 'email': 'my.friend@example.com'}] draft.subject = "Here's an attachment" diff --git a/tests/test_events.py b/tests/test_events.py index b7a85a5c..aadee601 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,12 +1,11 @@ import json import pytest -import responses import httpretty from httpretty import Response from nylas.client.errors import InvalidRequestError -body = { +BODY = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", "description": None, @@ -16,7 +15,7 @@ "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn", "object": "event", "owner": None, - "participants": [ ], + "participants": [], "read_only": False, "status": "confirmed", "title": "The rain song", @@ -30,7 +29,7 @@ @pytest.fixture def mock_event_create_response(api_url): - values = [Response(status=200, body=json.dumps(body)), + values = [Response(status=200, body=json.dumps(BODY)), Response(status=400, body='')] httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) @@ -43,8 +42,12 @@ def mock_event_create_response(api_url): @pytest.fixture def mock_event_create_notify_response(api_url): - httpretty.register_uri(httpretty.POST, api_url + '/events/?notify_participants=true&other_param=1', - body=json.dumps(body), status=200) + httpretty.register_uri( + httpretty.POST, + api_url + '/events/?notify_participants=true&other_param=1', + body=json.dumps(BODY), + status=200 + ) def blank_event(api_client): @@ -55,40 +58,37 @@ def blank_event(api_client): return event -def test_event_crud(api_client, mock_event_create_response): +@pytest.mark.usefixtures("mock_event_create_response") +def test_event_crud(api_client): httpretty.enable() - e1 = blank_event(api_client) - e1.save() - assert e1.id == 'cv4ei7syx10uvsxbs21ccsezf' + event1 = blank_event(api_client) + event1.save() + assert event1.id == 'cv4ei7syx10uvsxbs21ccsezf' - e1.title = 'blah' - e1.save() - assert e1.title == 'loaded from JSON' - assert e1.get('ignored') is None + event1.title = 'blah' + event1.save() + assert event1.title == 'loaded from JSON' + assert event1.get('ignored') is None # Third time should fail. - e2 = blank_event(api_client) - raised = False - try: - e2.save() - except InvalidRequestError: - raised = True - - assert raised is True + event2 = blank_event(api_client) + with pytest.raises(InvalidRequestError): + event2.save() httpretty.disable() -def test_event_notify(api_client, mock_event_create_notify_response): +@pytest.mark.usefixtures("mock_event_create_notify_response") +def test_event_notify(api_client): httpretty.enable() - e1 = blank_event(api_client) - e1.save(notify_participants='true', other_param='1') - assert e1.id == 'cv4ei7syx10uvsxbs21ccsezf' + event1 = blank_event(api_client) + event1.save(notify_participants='true', other_param='1') + assert event1.id == 'cv4ei7syx10uvsxbs21ccsezf' - qs = httpretty.last_request().querystring - assert qs['notify_participants'][0] == 'true' - assert qs['other_param'][0] == '1' + query = httpretty.last_request().querystring + assert query['notify_participants'][0] == 'true' + assert query['other_param'][0] == '1' httpretty.disable() diff --git a/tests/test_files.py b/tests/test_files.py index e024b26e..204250bb 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,9 +1,8 @@ import json import pytest -import responses import httpretty from httpretty import Response -from nylas.client.errors import InvalidRequestError, FileUploadError +from nylas.client.errors import FileUploadError def test_file_upload(api_client, api_url): diff --git a/tests/test_filter.py b/tests/test_filter.py index 042262ab..eaf8e75d 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,13 +1,10 @@ import json -import pytest import random -import responses import httpretty from httpretty import Response -from nylas.client.errors import InvalidRequestError -default_body = { +DEFAULT_BODY = { "busy": True, "calendar_id": "94rssh7bd3rmsxsp19kiocxze", "description": None, @@ -17,7 +14,7 @@ "namespace_id": "384uhp3aj8l7rpmv9s2y2rukn", "object": "event", "owner": None, - "participants": [ ], + "participants": [], "read_only": False, "status": "confirmed", "title": "The rain song", @@ -28,17 +25,21 @@ } } -body = [default_body for i in range(1, 51)] -body2 = [default_body for i in range(1, 23)] +BODY = [DEFAULT_BODY for i in range(1, 51)] +BODY2 = [DEFAULT_BODY for i in range(1, 23)] def test_no_filter(api_client, api_url): httpretty.enable() # httpretty kind of sucks and strips & parameters from the URL - values = [Response(status=200, body=json.dumps(body)), - Response(status=200, body=json.dumps(body2))] - httpretty.register_uri(httpretty.GET, api_url + '/events', responses=values) + values = [Response(status=200, body=json.dumps(BODY)), + Response(status=200, body=json.dumps(BODY2))] + httpretty.register_uri( + httpretty.GET, + api_url + '/events', + responses=values, + ) events = api_client.events.all() assert len(events) == 72 @@ -51,52 +52,60 @@ def test_two_filters(api_client, api_url): httpretty.enable() values2 = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, api_url + '/events?param1=a¶m2=b', responses=values2) + httpretty.register_uri( + httpretty.GET, + api_url + '/events?param1=a¶m2=b', + responses=values2, + ) events = api_client.events.where(param1='a', param2='b').all() - assert len(events) == 0 - qs = httpretty.last_request().querystring - assert qs['param1'][0] == 'a' - assert qs['param2'][0] == 'b' + assert len(events) == 0 # pylint: disable=len-as-condition + query = httpretty.last_request().querystring + assert query['param1'][0] == 'a' + assert query['param2'][0] == 'b' httpretty.disable() def test_no_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas', responses=values) - events = api_client.events.where({'in': 'Nylas'}).items() - for event in events: - pass - qs = httpretty.last_request().querystring - assert qs['in'][0] == 'Nylas' - assert qs['offset'][0] == '0' + httpretty.register_uri( + httpretty.GET, + api_url + '/events?in=Nylas', + responses=values, + ) + list(api_client.events.where({'in': 'Nylas'}).items()) + query = httpretty.last_request().querystring + assert query['in'][0] == 'Nylas' + assert query['offset'][0] == '0' httpretty.disable() def test_zero_offset(api_client, api_url): httpretty.enable() values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=0', responses=values) - events = api_client.events.where({'in': 'Nylas', 'offset': 0}).items() - for event in events: - pass - qs = httpretty.last_request().querystring - assert qs['in'][0] == 'Nylas' - assert qs['offset'][0] == '0' + httpretty.register_uri( + httpretty.GET, + api_url + '/events?in=Nylas&offset=0', + responses=values, + ) + list(api_client.events.where({'in': 'Nylas', 'offset': 0}).items()) + query = httpretty.last_request().querystring + assert query['in'][0] == 'Nylas' + assert query['offset'][0] == '0' httpretty.disable() def test_non_zero_offset(api_client, api_url): httpretty.enable() - offset = random.randint(1,1000) + offset = random.randint(1, 1000) values = [Response(status=200, body='[]')] - httpretty.register_uri(httpretty.GET, api_url + '/events?in=Nylas&offset=' + - str(offset), responses=values) - events = api_client.events.where({'in': 'Nylas', 'offset': offset}).items() - for event in events: - pass - qs = httpretty.last_request().querystring - assert qs['in'][0] == 'Nylas' - assert qs['offset'][0] == str(offset) + httpretty.register_uri( + httpretty.GET, + api_url + '/events?in=Nylas&offset=' + str(offset), + responses=values, + ) + list(api_client.events.where({'in': 'Nylas', 'offset': offset}).items()) + query = httpretty.last_request().querystring + assert query['in'][0] == 'Nylas' + assert query['offset'][0] == str(offset) httpretty.disable() - diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py index b78e9616..4df268bd 100644 --- a/tests/test_folder_labels.py +++ b/tests/test_folder_labels.py @@ -9,6 +9,7 @@ MOCK_ACCOUNT_ID = '4ennivvrcgsqytgybfk912dto' +# pylint: disable=redefined-outer-name @pytest.fixture def api_client(): @@ -258,7 +259,8 @@ def request_callback(request): @responses.activate -def test_list_labels(api_client, mock_labels): +@pytest.mark.usefixtures("mock_labels") +def test_list_labels(api_client): labels = api_client.labels labels = [l for l in labels] assert len(labels) == 5 @@ -266,7 +268,8 @@ def test_list_labels(api_client, mock_labels): @responses.activate -def test_get_label(api_client, mock_label): +@pytest.mark.usefixtures("mock_label") +def test_get_label(api_client): label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') assert label is not None assert isinstance(label, Label) @@ -274,7 +277,8 @@ def test_get_label(api_client, mock_label): @responses.activate -def test_get_change_folder(api_client, mock_folder): +@pytest.mark.usefixtures("mock_folder") +def test_get_change_folder(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') assert folder is not None assert isinstance(folder, Folder) @@ -285,7 +289,8 @@ def test_get_change_folder(api_client, mock_folder): @responses.activate -def test_messages(api_client, mock_messages): +@pytest.mark.usefixtures("mock_messages") +def test_messages(api_client): message = api_client.messages.first() assert len(message.labels) == 1 assert message.labels[0].display_name == 'Inbox' @@ -295,8 +300,8 @@ def test_messages(api_client, mock_messages): @responses.activate -def test_message_change(api_client, mock_account, mock_messages, - mock_message): +@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +def test_message_change(api_client): message = api_client.messages.first() message.star() assert message.starred is True @@ -320,9 +325,10 @@ def test_message_change(api_client, mock_account, mock_messages, @responses.activate -def test_thread_folder(api_client, mock_threads): +@pytest.mark.usefixtures("mock_threads") +def test_thread_folder(api_client): thread = api_client.threads.first() - assert len(thread.labels) == 0 + assert len(thread.labels) == 0 # pylint: disable=len-as-condition assert len(thread.folders) == 1 assert thread.folders[0].display_name == 'Inbox' assert not thread.unread @@ -330,8 +336,8 @@ def test_thread_folder(api_client, mock_threads): @responses.activate -def test_thread_change(api_client, mock_folder_account, - mock_threads, mock_thread): +@pytest.mark.usefixtures("mock_folder_account", "mock_threads", "mock_thread") +def test_thread_change(api_client): thread = api_client.threads.first() assert thread.starred diff --git a/tests/test_search.py b/tests/test_search.py index 6e84e2b0..499dc514 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,149 +1,164 @@ import json import pytest import responses -from nylas.client.errors import InvalidRequestError @pytest.fixture def mock_thread_search_response(api_url): - response_body = json.dumps( - [ - { - "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": "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", - "folders": [ - { - "name": "inbox", - "display_name": "INBOX", - "id": "f0idlvozkrpj3ihxze7obpivh" - }, - ], - "message_ids": [ - "251r594smznew6yhiocht2v29", - "7upzl8ss738iz8xf48lm84q3e", - "ah5wuphj3t83j260jqucm9a28" - ], - "draft_ids": [ - "251r594smznew6yhi12312saq" - ], - "version": 2 - } - ]) - - 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(api_url): - 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" + 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([ + { + "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": "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", - "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": { + ], + "snippet": snippet, + "folders": [ + { "name": "inbox", "display_name": "INBOX", "id": "f0idlvozkrpj3ihxze7obpivh" }, - "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", - "body": "....", - "files": [], - "events": [] - } - ]) + ], + "message_ids": [ + "251r594smznew6yhiocht2v29", + "7upzl8ss738iz8xf48lm84q3e", + "ah5wuphj3t83j260jqucm9a28" + ], + "draft_ids": [ + "251r594smznew6yhi12312saq" + ], + "version": 2 + } + ]) + + 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(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" + }, + "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": [] + } + ]) - responses.add(responses.GET, - api_url + '/messages/search?q=Pinot', - body=response_body, status=200, - content_type='application/json', - match_querystring=True) + responses.add( + responses.GET, + api_url + '/messages/search?q=Pinot', + body=response_body, + status=200, + content_type='application/json', + match_querystring=True + ) @responses.activate -def test_search_threads(api_client, mock_thread_search_response): +@pytest.mark.usefixtures("mock_thread_search_response") +def test_search_threads(api_client): threads = api_client.threads.search("Helena") assert len(threads) == 1 assert "Helena" in threads[0].snippet @responses.activate -def test_search_messages(api_client, mock_message_search_response): +@pytest.mark.usefixtures("mock_message_search_response") +def test_search_messages(api_client): messages = api_client.messages.search("Pinot") assert len(messages) == 2 assert "Pinot" in messages[0].snippet assert "Pinot" in messages[1].snippet @responses.activate -def test_search_drafts(api_client, mock_message_search_response): +@pytest.mark.usefixtures("mock_message_search_response") +def test_search_drafts(api_client): with pytest.raises(Exception): api_client.drafts.search("Pinot") diff --git a/tests/test_send_error_handling.py b/tests/test_send_error_handling.py index 1e470e3a..c83a9814 100644 --- a/tests/test_send_error_handling.py +++ b/tests/test_send_error_handling.py @@ -2,7 +2,6 @@ import re import pytest import responses -from nylas import APIClient from nylas.client.errors import ( MessageRejectedError, SendingQuotaExceededError, ServiceUnavailableError, ) @@ -27,7 +26,8 @@ def mock_sending_error(http_code, message, server_error=None): @responses.activate -def test_handle_message_rejected(api_client, mock_account, mock_save_draft): +@pytest.mark.usefixtures("mock_account", "mock_save_draft") +def test_handle_message_rejected(api_client): draft = api_client.drafts.create() error_message = 'Sending to all recipients failed' mock_sending_error(402, error_message) @@ -37,7 +37,8 @@ def test_handle_message_rejected(api_client, mock_account, mock_save_draft): @responses.activate -def test_handle_quota_exceeded(api_client, mock_account, mock_save_draft): +@pytest.mark.usefixtures("mock_account", "mock_save_draft") +def test_handle_quota_exceeded(api_client): draft = api_client.drafts.create() error_message = 'Daily sending quota exceeded' mock_sending_error(429, error_message) @@ -47,8 +48,8 @@ def test_handle_quota_exceeded(api_client, mock_account, mock_save_draft): @responses.activate -def test_handle_service_unavailable(api_client, mock_account, - mock_save_draft): +@pytest.mark.usefixtures("mock_account", "mock_save_draft") +def test_handle_service_unavailable(api_client): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' mock_sending_error(503, error_message) @@ -58,8 +59,8 @@ def test_handle_service_unavailable(api_client, mock_account, @responses.activate -def test_returns_server_error(api_client, mock_account, - mock_save_draft): +@pytest.mark.usefixtures("mock_account", "mock_save_draft") +def test_returns_server_error(api_client): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' reason = 'Rejected potential SPAM' @@ -73,8 +74,8 @@ def test_returns_server_error(api_client, mock_account, @responses.activate -def test_doesnt_return_server_error_if_not_defined(api_client, mock_account, - mock_save_draft): +@pytest.mark.usefixtures("mock_account", "mock_save_draft") +def test_doesnt_return_server_error_if_not_defined(api_client): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' mock_sending_error(503, error_message) From 0d4e26fe4bb61da4ec1065b0139cb99ce0bf8ce3 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 10:47:36 -0400 Subject: [PATCH 034/451] Travis needs an updated version of pytest --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4e9b8c27..ed360e62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.6" install: + - "travis_retry pip install -U pytest" - "python setup.py install" - "travis_retry pip install codecov" script: "python setup.py test -a --pylint" From 0a4818080646604b69204c13f2185ebecb4fa116 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 11:10:50 -0400 Subject: [PATCH 035/451] added --lint flag to python setup.py test --- .travis.yml | 2 +- setup.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed360e62..65c8a6d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,6 @@ install: - "travis_retry pip install -U pytest" - "python setup.py install" - "travis_retry pip install codecov" -script: "python setup.py test -a --pylint" +script: "python setup.py test --lint" after_success: - "codecov" diff --git a/setup.py b/setup.py index d5be36f3..88d009d7 100644 --- a/setup.py +++ b/setup.py @@ -13,16 +13,26 @@ class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] + user_options = [ + ('pytest-args=', 'a', "Arguments to pass to pytest"), + ('lint', None, "Enable linting with pylint"), + ] + + boolean_options = ['lint'] def initialize_options(self): TestCommand.initialize_options(self) - self.pytest_args = ['--cov', '--junitxml', './tests/output', 'tests/'] # pylint: disable=attribute-defined-outside-init + # pylint: disable=attribute-defined-outside-init + self.pytest_args = ['--cov', '--junitxml', './tests/output', 'tests/'] + self.lint = False def finalize_options(self): TestCommand.finalize_options(self) + # pylint: disable=attribute-defined-outside-init self.test_args = [] - self.test_suite = True # pylint: disable=attribute-defined-outside-init + self.test_suite = True + if self.lint: + self.pytest_args.append("--pylint") def run_tests(self): # import here, cause outside the eggs aren't loaded From a5d0c441ac52e9b8faafea0bc6479da8ad5b2fbd Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 14:24:50 -0400 Subject: [PATCH 036/451] remove duplicate API_URL --- tests/test_folder_labels.py | 159 ++++++++++++++++++++++-------------- 1 file changed, 99 insertions(+), 60 deletions(-) diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py index 4df268bd..e9f3c456 100644 --- a/tests/test_folder_labels.py +++ b/tests/test_folder_labels.py @@ -2,22 +2,14 @@ import re import pytest import responses -from nylas import APIClient from nylas.client.restful_models import Label, Folder -API_URL = 'http://localhost:2222' MOCK_ACCOUNT_ID = '4ennivvrcgsqytgybfk912dto' -# pylint: disable=redefined-outer-name @pytest.fixture -def api_client(): - return APIClient(None, None, None, API_URL) - - -@pytest.fixture -def mock_account(): +def mock_account(api_url): response_body = json.dumps( { "account_id": MOCK_ACCOUNT_ID, @@ -29,13 +21,18 @@ def mock_account(): "organization_unit": "label" } ) - responses.add(responses.GET, API_URL + '/account', - content_type='application/json', status=200, - body=response_body, match_querystring=True) + responses.add( + responses.GET, + api_url + '/account', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) @pytest.fixture -def mock_folder_account(): +def mock_folder_account(api_url): response_body = json.dumps( { "email_address": "ben.bitdiddle1861@office365.com", @@ -47,13 +44,18 @@ def mock_folder_account(): "organization_unit": "folder" } ) - responses.add(responses.GET, API_URL + '/account', - content_type='application/json', status=200, - body=response_body, match_querystring=True) + responses.add( + responses.GET, + api_url + '/account', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) @pytest.fixture -def mock_labels(): +def mock_labels(api_url): response_body = json.dumps([ { "display_name": "Important", @@ -91,14 +93,18 @@ def mock_labels(): "object": "label" } ]) - endpoint = re.compile(API_URL + '/labels.*') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) + endpoint = re.compile(api_url + '/labels.*') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body, + ) @pytest.fixture -def mock_label(): +def mock_label(api_url): response_body = json.dumps( { "display_name": "Important", @@ -108,14 +114,18 @@ def mock_label(): "object": "label" } ) - endpoint = re.compile(API_URL + '/labels/anuep8pe5ugmxrucchrzba2o8') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) + endpoint = re.compile(api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body, + ) @pytest.fixture -def mock_folder(): +def mock_folder(api_url): folder = { "display_name": "My Folder", "id": "anuep8pe5ug3xrupchwzba2o8", @@ -124,24 +134,32 @@ def mock_folder(): "object": "folder" } response_body = json.dumps(folder) - endpoint = re.compile(API_URL + '/folders/anuep8pe5ug3xrupchwzba2o8') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) + endpoint = re.compile(api_url + '/folders/anuep8pe5ug3xrupchwzba2o8') + responses.add( + responses.GET, + endpoint, + 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)) - responses.add_callback(responses.PUT, endpoint, - content_type='application/json', - callback=request_callback) + + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback, + ) @pytest.fixture -def mock_messages(): +def mock_messages(api_url): response_body = json.dumps([ { "id": "1234", @@ -159,14 +177,17 @@ def mock_messages(): "unread": True } ]) - endpoint = re.compile(API_URL + '/messages') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) + endpoint = re.compile(api_url + '/messages') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body) @pytest.fixture -def mock_message(): +def mock_message(api_url): base_msg = { "id": "1234", "subject": "Test Message", @@ -192,17 +213,24 @@ def request_callback(request): base_msg['labels'] = labels return (200, {}, json.dumps(base_msg)) - endpoint = re.compile(API_URL + '/messages/1234') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) - responses.add_callback(responses.PUT, endpoint, - content_type='application/json', - callback=request_callback) + endpoint = re.compile(api_url + '/messages/1234') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback + ) @pytest.fixture -def mock_threads(): +def mock_threads(api_url): response_body = json.dumps([ { "id": "5678", @@ -218,14 +246,18 @@ def mock_threads(): "unread": False } ]) - endpoint = re.compile(API_URL + '/threads') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) + endpoint = re.compile(api_url + '/threads') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) @pytest.fixture -def mock_thread(): +def mock_thread(api_url): base_thrd = { "id": "5678", "subject": "Test Thread", @@ -249,13 +281,20 @@ def request_callback(request): base_thrd['folders'] = [folder] return (200, {}, json.dumps(base_thrd)) - endpoint = re.compile(API_URL + '/threads/5678') - responses.add(responses.GET, endpoint, - content_type='application/json', status=200, - body=response_body) - responses.add_callback(responses.PUT, endpoint, - content_type='application/json', - callback=request_callback) + endpoint = re.compile(api_url + '/threads/5678') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback, + ) @responses.activate From 5537ae34e5d5fd059f9920d0e3cf8ad566a2d132 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 15:04:46 -0400 Subject: [PATCH 037/451] Move all mocks to conftest.py --- tests/conftest.py | 650 +++++++++++++++++++++++++++++- tests/test_drafts.py | 139 ------- tests/test_events.py | 45 --- tests/test_filter.py | 36 +- tests/test_folder_labels.py | 292 -------------- tests/test_search.py | 139 ------- tests/test_send_error_handling.py | 26 +- 7 files changed, 664 insertions(+), 663 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 12fc6cac..150f5e69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,15 +2,45 @@ import json import pytest import responses +import httpretty from nylas import APIClient # pylint: disable=redefined-outer-name + +@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 api_url(): return 'http://localhost:2222' +@pytest.fixture +def account_id(): + return '4ennivvrcgsqytgybfk912dto' + + @pytest.fixture def api_client(api_url): return APIClient(None, None, None, api_url) @@ -28,9 +58,14 @@ def mock_account(api_url): "provider": "gmail" } ]) - responses.add(responses.GET, api_url + '/n?limit=1&offset=0', - content_type='application/json', status=200, - body=response_body, match_querystring=True) + responses.add( + responses.GET, + api_url + '/n?limit=1&offset=0', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) @pytest.fixture @@ -40,6 +75,609 @@ def mock_save_draft(api_url): "id": "4dl0ni6vxomazo73r5oydo16k", "version": "4dw0ni6txomazo33r5ozdo16j" }) - responses.add(responses.POST, save_endpoint, - content_type='application/json', status=200, - body=response_body, match_querystring=True) + responses.add( + responses.POST, + save_endpoint, + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) + + +@pytest.fixture +def mock_account(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" + } + ) + responses.add( + responses.GET, + api_url + '/account', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) + + +@pytest.fixture +def mock_folder_account(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" + } + ) + responses.add( + responses.GET, + api_url + '/account', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) + + +@pytest.fixture +def mock_labels(api_url, account_id): + response_body = json.dumps([ + { + "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" + } + ]) + endpoint = re.compile(api_url + '/labels.*') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body, + ) + + +@pytest.fixture +def mock_label(api_url, account_id): + response_body = json.dumps( + { + "display_name": "Important", + "id": "anuep8pe5ugmxrucchrzba2o8", + "name": "important", + "account_id": account_id, + "object": "label" + } + ) + endpoint = re.compile(api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body, + ) + + +@pytest.fixture +def mock_folder(api_url, account_id): + folder = { + "display_name": "My Folder", + "id": "anuep8pe5ug3xrupchwzba2o8", + "name": None, + "account_id": account_id, + "object": "folder" + } + response_body = json.dumps(folder) + endpoint = re.compile(api_url + '/folders/anuep8pe5ug3xrupchwzba2o8') + responses.add( + responses.GET, + endpoint, + 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)) + + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback, + ) + + + +@pytest.fixture +def mock_messages(api_url, account_id): + response_body = json.dumps([ + { + "id": "1234", + "subject": "Test Message", + "account_id": account_id, + "object": "message", + "labels": [ + { + "name": "inbox", + "display_name": "Inbox", + "id": "abcd" + } + ], + "starred": False, + "unread": True + } + ]) + endpoint = re.compile(api_url + '/messages') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body) + + +@pytest.fixture +def mock_message(api_url, account_id): + base_msg = { + "id": "1234", + "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') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback + ) + + +@pytest.fixture +def mock_threads(api_url, account_id): + response_body = json.dumps([ + { + "id": "5678", + "subject": "Test Thread", + "account_id": account_id, + "object": "thread", + "folders": [{ + "name": "inbox", + "display_name": "Inbox", + "id": "abcd" + }], + "starred": True, + "unread": False + } + ]) + endpoint = re.compile(api_url + '/threads') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + + +@pytest.fixture +def mock_thread(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 + } + 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') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback, + ) + + +@pytest.fixture +def mock_draft_saved_response(api_url): + response_body = json.dumps({ + "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 + }) + + responses.add( + responses.POST, + api_url + '/drafts/', + content_type='application/json', + status=200, + body=response_body, + match_querystring=True + ) + + +@pytest.fixture +def mock_draft_updated_response(api_url): + body = { + "bcc": [], + "body": "", + "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": "Stay polish, stay hungary", + "thread_id": "clm33kapdxkposgltof845v9s", + "to": [ + { + "email": "helena@nylas.com", + "name": "Helena Handbasket" + } + ], + "unread": False, + "version": 0 + } + + responses.add( + responses.PUT, + api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', + content_type='application/json', + status=200, + body=json.dumps(body), + match_querystring=True + ) + + body['subject'] = 'Update #2' + url = api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param' + responses.add( + responses.PUT, + url, + content_type='application/json', + status=200, + body=json.dumps(body), + match_querystring=True + ) + + +@pytest.fixture +def mock_draft_sent_response(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' + assert payload['version'] == 0 + return values.pop() + + responses.add_callback( + responses.POST, + api_url + '/send/', + callback=callback, + content_type='application/json' + ) + + +@pytest.fixture +def mock_event_create_response(api_url, message_body): + values = [ + httpretty.Response(status=200, body=json.dumps(message_body)), + httpretty.Response(status=400, body=''), + ] + + httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) + + body = json.dumps({'title': 'loaded from JSON', 'ignored': 'ignored'}) + put_values = [ + httpretty.Response(status=200, body=body) + ] + httpretty.register_uri( + httpretty.PUT, + api_url + '/events/cv4ei7syx10uvsxbs21ccsezf', + responses=put_values, + ) + + +@pytest.fixture +def mock_event_create_notify_response(api_url, message_body): + httpretty.register_uri( + httpretty.POST, + api_url + '/events/?notify_participants=true&other_param=1', + body=json.dumps(message_body), + status=200 + ) + + + +@pytest.fixture +def mock_thread_search_response(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([ + { + "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 + } + ]) + + 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(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" + }, + "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": [] + } + ]) + + responses.add( + responses.GET, + api_url + '/messages/search?q=Pinot', + body=response_body, + status=200, + content_type='application/json', + match_querystring=True + ) diff --git a/tests/test_drafts.py b/tests/test_drafts.py index aa15871f..7aeb00f7 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -1,147 +1,8 @@ -import json import pytest import responses from nylas.client.errors import InvalidRequestError -@pytest.fixture -def mock_draft_saved_response(api_url): - response_body = json.dumps({ - "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 - }) - - responses.add( - responses.POST, - api_url + '/drafts/', - content_type='application/json', - status=200, - body=response_body, - match_querystring=True - ) - - -@pytest.fixture -def mock_draft_updated_response(api_url): - body = { - "bcc": [], - "body": "", - "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": "Stay polish, stay hungary", - "thread_id": "clm33kapdxkposgltof845v9s", - "to": [ - { - "email": "helena@nylas.com", - "name": "Helena Handbasket" - } - ], - "unread": False, - "version": 0 - } - - responses.add( - responses.PUT, - api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', - content_type='application/json', - status=200, - body=json.dumps(body), - match_querystring=True - ) - - body['subject'] = 'Update #2' - url = api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param' - responses.add( - responses.PUT, - url, - content_type='application/json', - status=200, - body=json.dumps(body), - match_querystring=True - ) - - -@pytest.fixture -def mock_draft_sent_response(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' - assert payload['version'] == 0 - return values.pop() - - responses.add_callback( - responses.POST, - api_url + '/send/', - callback=callback, - content_type='application/json' - ) - - @responses.activate @pytest.mark.usefixtures( "mock_draft_saved_response", "mock_draft_updated_response", diff --git a/tests/test_events.py b/tests/test_events.py index aadee601..babc9e9b 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -5,51 +5,6 @@ from nylas.client.errors import InvalidRequestError -BODY = { - "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 mock_event_create_response(api_url): - values = [Response(status=200, body=json.dumps(BODY)), - Response(status=400, body='')] - - httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) - put_values = [Response(status=200, - body=json.dumps({'title': 'loaded from JSON', - 'ignored': 'ignored'}))] - httpretty.register_uri(httpretty.PUT, api_url + '/events/cv4ei7syx10uvsxbs21ccsezf', - responses=put_values) - - -@pytest.fixture -def mock_event_create_notify_response(api_url): - httpretty.register_uri( - httpretty.POST, - api_url + '/events/?notify_participants=true&other_param=1', - body=json.dumps(BODY), - status=200 - ) - - def blank_event(api_client): event = api_client.events.create() event.title = "Paris-Brest" diff --git a/tests/test_filter.py b/tests/test_filter.py index eaf8e75d..f94efa8d 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -4,37 +4,17 @@ from httpretty import Response -DEFAULT_BODY = { - "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 - } -} - -BODY = [DEFAULT_BODY for i in range(1, 51)] -BODY2 = [DEFAULT_BODY for i in range(1, 23)] - - -def test_no_filter(api_client, api_url): +def test_no_filter(api_client, api_url, message_body): httpretty.enable() + message_body_list_50 = [message_body for i in range(1, 51)] + message_body_list_22 = [message_body for i in range(1, 23)] + # httpretty kind of sucks and strips & parameters from the URL - values = [Response(status=200, body=json.dumps(BODY)), - Response(status=200, body=json.dumps(BODY2))] + values = [ + Response(status=200, body=json.dumps(message_body_list_50)), + Response(status=200, body=json.dumps(message_body_list_22)), + ] httpretty.register_uri( httpretty.GET, api_url + '/events', diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py index e9f3c456..72aaa863 100644 --- a/tests/test_folder_labels.py +++ b/tests/test_folder_labels.py @@ -5,298 +5,6 @@ from nylas.client.restful_models import Label, Folder -MOCK_ACCOUNT_ID = '4ennivvrcgsqytgybfk912dto' - - -@pytest.fixture -def mock_account(api_url): - response_body = json.dumps( - { - "account_id": MOCK_ACCOUNT_ID, - "email_address": "ben.bitdiddle1861@gmail.com", - "id": MOCK_ACCOUNT_ID, - "name": "Ben Bitdiddle", - "object": "account", - "provider": "gmail", - "organization_unit": "label" - } - ) - responses.add( - responses.GET, - api_url + '/account', - content_type='application/json', - status=200, - body=response_body, - match_querystring=True - ) - - -@pytest.fixture -def mock_folder_account(api_url): - response_body = json.dumps( - { - "email_address": "ben.bitdiddle1861@office365.com", - "id": MOCK_ACCOUNT_ID, - "name": "Ben Bitdiddle", - "account_id": MOCK_ACCOUNT_ID, - "object": "account", - "provider": "eas", - "organization_unit": "folder" - } - ) - responses.add( - responses.GET, - api_url + '/account', - content_type='application/json', - status=200, - body=response_body, - match_querystring=True - ) - - -@pytest.fixture -def mock_labels(api_url): - response_body = json.dumps([ - { - "display_name": "Important", - "id": "anuep8pe5ugmxrucchrzba2o8", - "name": "important", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - }, - { - "display_name": "Trash", - "id": "f1xgowbgcehk235xiy3c3ek42", - "name": "trash", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - }, - { - "display_name": "Sent Mail", - "id": "ah14wp5fvypvjjnplh7nxgb4h", - "name": "sent", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - }, - { - "display_name": "All Mail", - "id": "ah14wp5fvypvjjnplh7nxgb4h", - "name": "all", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - }, - { - "display_name": "Inbox", - "id": "dc11kl3s9lj4760g6zb36spms", - "name": "inbox", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - } - ]) - endpoint = re.compile(api_url + '/labels.*') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body, - ) - - -@pytest.fixture -def mock_label(api_url): - response_body = json.dumps( - { - "display_name": "Important", - "id": "anuep8pe5ugmxrucchrzba2o8", - "name": "important", - "account_id": MOCK_ACCOUNT_ID, - "object": "label" - } - ) - endpoint = re.compile(api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body, - ) - - -@pytest.fixture -def mock_folder(api_url): - folder = { - "display_name": "My Folder", - "id": "anuep8pe5ug3xrupchwzba2o8", - "name": None, - "account_id": MOCK_ACCOUNT_ID, - "object": "folder" - } - response_body = json.dumps(folder) - endpoint = re.compile(api_url + '/folders/anuep8pe5ug3xrupchwzba2o8') - responses.add( - responses.GET, - endpoint, - 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)) - - responses.add_callback( - responses.PUT, - endpoint, - content_type='application/json', - callback=request_callback, - ) - - - -@pytest.fixture -def mock_messages(api_url): - response_body = json.dumps([ - { - "id": "1234", - "subject": "Test Message", - "account_id": MOCK_ACCOUNT_ID, - "object": "message", - "labels": [ - { - "name": "inbox", - "display_name": "Inbox", - "id": "abcd" - } - ], - "starred": False, - "unread": True - } - ]) - endpoint = re.compile(api_url + '/messages') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body) - - -@pytest.fixture -def mock_message(api_url): - base_msg = { - "id": "1234", - "subject": "Test Message", - "account_id": MOCK_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') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body - ) - responses.add_callback( - responses.PUT, - endpoint, - content_type='application/json', - callback=request_callback - ) - - -@pytest.fixture -def mock_threads(api_url): - response_body = json.dumps([ - { - "id": "5678", - "subject": "Test Thread", - "account_id": MOCK_ACCOUNT_ID, - "object": "thread", - "folders": [{ - "name": "inbox", - "display_name": "Inbox", - "id": "abcd" - }], - "starred": True, - "unread": False - } - ]) - endpoint = re.compile(api_url + '/threads') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body - ) - - -@pytest.fixture -def mock_thread(api_url): - base_thrd = { - "id": "5678", - "subject": "Test Thread", - "account_id": MOCK_ACCOUNT_ID, - "object": "thread", - "folders": [{ - "name": "inbox", - "display_name": "Inbox", - "id": "abcd" - }], - "starred": True, - "unread": False - } - 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') - responses.add( - responses.GET, - endpoint, - content_type='application/json', - status=200, - body=response_body - ) - responses.add_callback( - responses.PUT, - endpoint, - content_type='application/json', - callback=request_callback, - ) - - @responses.activate @pytest.mark.usefixtures("mock_labels") def test_list_labels(api_client): diff --git a/tests/test_search.py b/tests/test_search.py index 499dc514..d551393a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -2,145 +2,6 @@ import pytest import responses -@pytest.fixture -def mock_thread_search_response(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([ - { - "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 - } - ]) - - 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(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" - }, - "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": [] - } - ]) - - responses.add( - responses.GET, - api_url + '/messages/search?q=Pinot', - body=response_body, - status=200, - content_type='application/json', - match_querystring=True - ) - @responses.activate @pytest.mark.usefixtures("mock_thread_search_response") diff --git a/tests/test_send_error_handling.py b/tests/test_send_error_handling.py index c83a9814..1bd3d0f4 100644 --- a/tests/test_send_error_handling.py +++ b/tests/test_send_error_handling.py @@ -6,11 +6,9 @@ MessageRejectedError, SendingQuotaExceededError, ServiceUnavailableError, ) -API_URL = 'http://localhost:2222' - -def mock_sending_error(http_code, message, server_error=None): - send_endpoint = re.compile(API_URL + '/send') +def mock_sending_error(http_code, message, api_url, server_error=None): + send_endpoint = re.compile(api_url + '/send') response_body = { "type": "api_error", "message": message @@ -27,10 +25,10 @@ def mock_sending_error(http_code, message, server_error=None): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_message_rejected(api_client): +def test_handle_message_rejected(api_client, api_url): draft = api_client.drafts.create() error_message = 'Sending to all recipients failed' - mock_sending_error(402, error_message) + mock_sending_error(402, error_message, api_url=api_url) with pytest.raises(MessageRejectedError) as exc: draft.send() assert exc.value.message == error_message @@ -38,10 +36,10 @@ def test_handle_message_rejected(api_client): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_quota_exceeded(api_client): +def test_handle_quota_exceeded(api_client, api_url): draft = api_client.drafts.create() error_message = 'Daily sending quota exceeded' - mock_sending_error(429, error_message) + mock_sending_error(429, error_message, api_url=api_url) with pytest.raises(SendingQuotaExceededError) as exc: draft.send() assert exc.value.message == error_message @@ -49,10 +47,10 @@ def test_handle_quota_exceeded(api_client): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_service_unavailable(api_client): +def test_handle_service_unavailable(api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' - mock_sending_error(503, error_message) + mock_sending_error(503, error_message, api_url=api_url) with pytest.raises(ServiceUnavailableError) as exc: draft.send() assert exc.value.message == error_message @@ -60,11 +58,11 @@ def test_handle_service_unavailable(api_client): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_returns_server_error(api_client): +def test_returns_server_error(api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' reason = 'Rejected potential SPAM' - mock_sending_error(503, error_message, + mock_sending_error(503, error_message, api_url=api_url, server_error=reason) with pytest.raises(ServiceUnavailableError) as exc: draft.send() @@ -75,10 +73,10 @@ def test_returns_server_error(api_client): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_doesnt_return_server_error_if_not_defined(api_client): +def test_doesnt_return_server_error_if_not_defined(api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' - mock_sending_error(503, error_message) + mock_sending_error(503, error_message, api_url=api_url) with pytest.raises(ServiceUnavailableError) as exc: draft.send() assert exc.value.message == error_message From a7e81e303cc958290e5d7f58a5cce8aa489e8477 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 15:11:44 -0400 Subject: [PATCH 038/451] fix pylint --- tests/conftest.py | 22 ---------------------- tests/test_events.py | 2 -- tests/test_filter.py | 4 ++-- tests/test_folder_labels.py | 2 -- tests/test_search.py | 1 - 5 files changed, 2 insertions(+), 29 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 150f5e69..a735a621 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,28 +46,6 @@ def api_client(api_url): return APIClient(None, None, None, api_url) -@pytest.fixture -def mock_account(api_url): - response_body = json.dumps([ - { - "account_id": "4dl0ni6vxomazo73r5ozdo16j", - "email_address": "ben.bitdiddle1861@gmail.com", - "id": "4dl0ni6vxomazo73r5ozdo16j", - "name": "Ben Bitdiddle", - "object": "account", - "provider": "gmail" - } - ]) - responses.add( - responses.GET, - api_url + '/n?limit=1&offset=0', - content_type='application/json', - status=200, - body=response_body, - match_querystring=True - ) - - @pytest.fixture def mock_save_draft(api_url): save_endpoint = re.compile(api_url + '/drafts/') diff --git a/tests/test_events.py b/tests/test_events.py index babc9e9b..095d8125 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,7 +1,5 @@ -import json import pytest import httpretty -from httpretty import Response from nylas.client.errors import InvalidRequestError diff --git a/tests/test_filter.py b/tests/test_filter.py index f94efa8d..fc3c263b 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -7,8 +7,8 @@ def test_no_filter(api_client, api_url, message_body): httpretty.enable() - message_body_list_50 = [message_body for i in range(1, 51)] - message_body_list_22 = [message_body for i in range(1, 23)] + message_body_list_50 = [message_body for _ in range(1, 51)] + message_body_list_22 = [message_body for _ in range(1, 23)] # httpretty kind of sucks and strips & parameters from the URL values = [ diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py index 72aaa863..534b7d96 100644 --- a/tests/test_folder_labels.py +++ b/tests/test_folder_labels.py @@ -1,5 +1,3 @@ -import json -import re import pytest import responses from nylas.client.restful_models import Label, Folder diff --git a/tests/test_search.py b/tests/test_search.py index d551393a..0d084bf7 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,4 +1,3 @@ -import json import pytest import responses From 5fd88343ce23eab9c0b660df62a4a49d923bfc4c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 15:08:54 -0400 Subject: [PATCH 039/451] remove unneeded generate_id() function --- examples/upload_and_download.py | 5 +---- examples/upload_files_in_dir.py | 3 +-- nylas/client/client.py | 5 ++--- nylas/client/util.py | 16 ---------------- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/examples/upload_and_download.py b/examples/upload_and_download.py index 80ef3f13..3e98a0d8 100755 --- a/examples/upload_and_download.py +++ b/examples/upload_and_download.py @@ -2,15 +2,12 @@ from __future__ import print_function import time from nylas import APIClient -from nylas.util import generate_id APP_ID = '[YOUR_APP_ID]' APP_SECRET = '[YOUR_APP_SECRET]' ACCESS_TOKEN = '[YOUR_ACCESS_TOKEN]' client = APIClient(APP_ID, APP_SECRET, ACCESS_TOKEN) -subject = generate_id() - f = open('test.py', 'r') data = f.read() f.close() @@ -22,7 +19,7 @@ # Create a new draft draft = client.drafts.create() draft.to = [{'name': 'Charles Gruenwald', 'email': 'nylastestempty@gmail.com'}] -draft.subject = subject +draft.subject = 'nylas test' draft.body = "" draft.attach(myfile) draft.send() diff --git a/examples/upload_files_in_dir.py b/examples/upload_files_in_dir.py index 8f49ee86..0a8b4858 100755 --- a/examples/upload_files_in_dir.py +++ b/examples/upload_files_in_dir.py @@ -3,14 +3,13 @@ import os import time from nylas import APIClient -from nylas.util import generate_id APP_ID = '[YOUR_APP_ID]' APP_SECRET = '[YOUR_APP_SECRET]' ACCESS_TOKEN = '[YOUR_ACCESS_TOKEN]' client = APIClient(APP_ID, APP_SECRET, ACCESS_TOKEN) -subject = generate_id() +subject = 'nylas test' # Create a new draft draft = client.drafts.create() draft.to = [{'name': 'Nylas PythonSDK', 'email': 'nylastestempty@gmail.com'}] diff --git a/nylas/client/client.py b/nylas/client/client.py index 52f0028f..2224a3fb 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -6,7 +6,7 @@ import requests from six.moves.urllib.parse import urlencode from nylas._client_sdk_version import __VERSION__ -from nylas.client.util import url_concat, generate_id +from nylas.client.util import url_concat from nylas.client.restful_model_collection import RestfulModelCollection from nylas.client.restful_models import ( Calendar, Contact, Event, Message, Thread, File, @@ -145,8 +145,7 @@ def authentication_url(self, redirect_uri, login_hint=''): 'client_id': self.app_id, 'response_type': 'code', 'scope': 'email', - 'login_hint': login_hint, - 'state': generate_id()} + 'login_hint': login_hint} return url_concat(self.authorize_url, args) diff --git a/nylas/client/util.py b/nylas/client/util.py index 357637cd..283a23ff 100644 --- a/nylas/client/util.py +++ b/nylas/client/util.py @@ -1,5 +1,3 @@ -from uuid import uuid4 -from struct import unpack from six.moves.urllib.parse import urlencode @@ -30,17 +28,3 @@ def url_concat(url, args, fragments=None): args_tail += urlencode(args) return url + args_tail + fragment_tail - - -def generate_id(): - a, b = unpack('>QQ', uuid4().bytes) # pylint: disable=invalid-name - num = a << 64 | b - - alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - - base36 = '' - while num: - num, i = divmod(num, 36) - base36 = alphabet[i] + base36 - - return base36 From 1212f75723b8259149c3ef057cde6654371ba2fd Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 15:57:51 -0400 Subject: [PATCH 040/451] Pass empty state to authentication_url --- nylas/client/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 2224a3fb..81a94fd0 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -145,7 +145,8 @@ def authentication_url(self, redirect_uri, login_hint=''): 'client_id': self.app_id, 'response_type': 'code', 'scope': 'email', - 'login_hint': login_hint} + 'login_hint': login_hint, + 'state': ''} return url_concat(self.authorize_url, args) From c554d7102ac411f1f41a2766f2c40ec3a1d56ff9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 15:34:08 -0400 Subject: [PATCH 041/451] organize test files --- tests/test_folder_labels.py | 94 ------------------------------------- tests/test_folders.py | 15 ++++++ tests/test_labels.py | 21 +++++++++ tests/test_messages.py | 61 ++++++++++++++++++++++++ tests/test_threads.py | 27 +++++++++++ 5 files changed, 124 insertions(+), 94 deletions(-) delete mode 100644 tests/test_folder_labels.py create mode 100644 tests/test_folders.py create mode 100644 tests/test_labels.py create mode 100644 tests/test_messages.py create mode 100644 tests/test_threads.py diff --git a/tests/test_folder_labels.py b/tests/test_folder_labels.py deleted file mode 100644 index 534b7d96..00000000 --- a/tests/test_folder_labels.py +++ /dev/null @@ -1,94 +0,0 @@ -import pytest -import responses -from nylas.client.restful_models import Label, Folder - - -@responses.activate -@pytest.mark.usefixtures("mock_labels") -def test_list_labels(api_client): - labels = api_client.labels - labels = [l for l in labels] - assert len(labels) == 5 - assert all(isinstance(x, Label) for x in labels) - - -@responses.activate -@pytest.mark.usefixtures("mock_label") -def test_get_label(api_client): - label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') - assert label is not None - assert isinstance(label, Label) - assert label.display_name == 'Important' - - -@responses.activate -@pytest.mark.usefixtures("mock_folder") -def test_get_change_folder(api_client): - folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') - assert folder is not None - assert isinstance(folder, Folder) - assert folder.display_name == 'My Folder' - folder.display_name = 'My New Folder' - folder.save() - assert folder.display_name == 'My New Folder' - - -@responses.activate -@pytest.mark.usefixtures("mock_messages") -def test_messages(api_client): - message = api_client.messages.first() - assert len(message.labels) == 1 - assert message.labels[0].display_name == 'Inbox' - assert message.folder is None - assert message.unread - assert not message.starred - - -@responses.activate -@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") -def test_message_change(api_client): - message = api_client.messages.first() - message.star() - assert message.starred is True - message.unstar() - assert message.starred is False - message.mark_as_read() - assert message - - message.add_label('fghj') - msg_labels = [l.id for l in message.labels] - assert 'abcd' in msg_labels - assert 'fghj' in msg_labels - message.remove_label('fghj') - msg_labels = [l.id for l in message.labels] - assert 'abcd' in msg_labels - assert 'fghj' not in msg_labels - - # Test that folders don't do anything when labels are in effect - message.update_folder('zxcv') - assert message.folder is None - - -@responses.activate -@pytest.mark.usefixtures("mock_threads") -def test_thread_folder(api_client): - thread = api_client.threads.first() - assert len(thread.labels) == 0 # pylint: disable=len-as-condition - assert len(thread.folders) == 1 - assert thread.folders[0].display_name == 'Inbox' - assert not thread.unread - assert thread.starred - - -@responses.activate -@pytest.mark.usefixtures("mock_folder_account", "mock_threads", "mock_thread") -def test_thread_change(api_client): - thread = api_client.threads.first() - - assert thread.starred - thread.unstar() - assert not thread.starred - - thread.update_folder('qwer') - assert len(thread.folders) == 1 - assert thread.folders[0].id == 'qwer' diff --git a/tests/test_folders.py b/tests/test_folders.py new file mode 100644 index 00000000..1645e98f --- /dev/null +++ b/tests/test_folders.py @@ -0,0 +1,15 @@ +import pytest +import responses +from nylas.client.restful_models import Folder + + +@responses.activate +@pytest.mark.usefixtures("mock_folder") +def test_get_change_folder(api_client): + folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') + assert folder is not None + assert isinstance(folder, Folder) + assert folder.display_name == 'My Folder' + folder.display_name = 'My New Folder' + folder.save() + assert folder.display_name == 'My New Folder' diff --git a/tests/test_labels.py b/tests/test_labels.py new file mode 100644 index 00000000..48acd86a --- /dev/null +++ b/tests/test_labels.py @@ -0,0 +1,21 @@ +import pytest +import responses +from nylas.client.restful_models import Label + + +@responses.activate +@pytest.mark.usefixtures("mock_labels") +def test_list_labels(api_client): + labels = api_client.labels + labels = [l for l in labels] + assert len(labels) == 5 + assert all(isinstance(x, Label) for x in labels) + + +@responses.activate +@pytest.mark.usefixtures("mock_label") +def test_get_label(api_client): + label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') + assert label is not None + assert isinstance(label, Label) + assert label.display_name == 'Important' diff --git a/tests/test_messages.py b/tests/test_messages.py new file mode 100644 index 00000000..6ebc47fa --- /dev/null +++ b/tests/test_messages.py @@ -0,0 +1,61 @@ +import json +import six +import pytest +import responses + + +@responses.activate +@pytest.mark.usefixtures("mock_messages") +def test_messages(api_client): + message = api_client.messages.first() + assert len(message.labels) == 1 + assert message.labels[0].display_name == 'Inbox' + assert message.folder is None + assert message.unread + assert not message.starred + + +@responses.activate +@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +def test_message_change(api_client): + message = api_client.messages.first() + message.star() + assert message.starred is True + message.unstar() + assert message.starred is False + message.mark_as_read() + assert message + + message.add_label('fghj') + msg_labels = [l.id for l in message.labels] + assert 'abcd' in msg_labels + assert 'fghj' in msg_labels + message.remove_label('fghj') + msg_labels = [l.id for l in message.labels] + assert 'abcd' in msg_labels + assert 'fghj' not in msg_labels + + # Test that folders don't do anything when labels are in effect + message.update_folder('zxcv') + assert message.folder is None + + +@responses.activate +@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +def test_message_raw(api_client, account_id): + message = api_client.messages.first() + assert isinstance(message.raw, six.binary_type) + parsed = json.loads(message.raw) + assert parsed == [{ + "object": "message", + "account_id": account_id, + "labels": [{ + "display_name": "Inbox", + "name": "inbox", + "id": "abcd", + }], + "starred": False, + "unread": True, + "id": "1234", + "subject": "Test Message" + }] diff --git a/tests/test_threads.py b/tests/test_threads.py new file mode 100644 index 00000000..48dc5900 --- /dev/null +++ b/tests/test_threads.py @@ -0,0 +1,27 @@ +import pytest +import responses + + +@responses.activate +@pytest.mark.usefixtures("mock_threads") +def test_thread_folder(api_client): + thread = api_client.threads.first() + assert len(thread.labels) == 0 # pylint: disable=len-as-condition + assert len(thread.folders) == 1 + assert thread.folders[0].display_name == 'Inbox' + assert not thread.unread + assert thread.starred + + +@responses.activate +@pytest.mark.usefixtures("mock_folder_account", "mock_threads", "mock_thread") +def test_thread_change(api_client): + thread = api_client.threads.first() + + assert thread.starred + thread.unstar() + assert not thread.starred + + thread.update_folder('qwer') + assert len(thread.folders) == 1 + assert thread.folders[0].id == 'qwer' From 068081db2d11234b6471ccfc77e6a2189a4bded1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 16:37:29 -0400 Subject: [PATCH 042/451] thread.messages and thread.drafts --- tests/conftest.py | 39 +++++++++++++++++++++++++++++++++++++++ tests/test_threads.py | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index a735a621..23733078 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -352,6 +352,45 @@ def request_callback(request): ) +@pytest.fixture +def mock_drafts(api_url): + response_body = json.dumps([{ + "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 + }]) + + responses.add( + responses.GET, + api_url + '/drafts', + content_type='application/json', + status=200, + body=response_body, + ) + + @pytest.fixture def mock_draft_saved_response(api_url): response_body = json.dumps({ diff --git a/tests/test_threads.py b/tests/test_threads.py index 48dc5900..a7e449bc 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,5 +1,6 @@ import pytest import responses +from nylas.client.restful_models import Message, Draft @responses.activate @@ -25,3 +26,21 @@ def test_thread_change(api_client): thread.update_folder('qwer') assert len(thread.folders) == 1 assert thread.folders[0].id == 'qwer' + + +@responses.activate +@pytest.mark.usefixtures("mock_threads", "mock_messages") +def test_thread_messages(api_client): + thread = api_client.threads.first() + assert thread.messages + assert all(isinstance(message, Message) + for message in thread.messages) + + +@responses.activate +@pytest.mark.usefixtures("mock_threads", "mock_drafts") +def test_thread_drafts(api_client): + thread = api_client.threads.first() + assert thread.drafts + assert all(isinstance(draft, Draft) + for draft in thread.drafts) From d381321a9ff1d3d7a4e64a6d9d36677498baff20 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 16:42:40 -0400 Subject: [PATCH 043/451] message.mark_as_read() --- tests/test_messages.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/test_messages.py b/tests/test_messages.py index 6ebc47fa..94b4c6cf 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -17,15 +17,31 @@ def test_messages(api_client): @responses.activate @pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") -def test_message_change(api_client): +def test_message_stars(api_client): message = api_client.messages.first() + assert message.starred is False message.star() assert message.starred is True message.unstar() assert message.starred is False + +@responses.activate +@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +def test_message_read(api_client): + message = api_client.messages.first() + assert message.unread is True message.mark_as_read() - assert message + assert message.unread is False + message.mark_as_unread() + assert message.unread is True + # mark_as_seen() is a synonum for mark_as_read() + message.mark_as_seen() + assert message.unread is False +@responses.activate +@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +def test_message_labels(api_client): + message = api_client.messages.first() message.add_label('fghj') msg_labels = [l.id for l in message.labels] assert 'abcd' in msg_labels From e21d2f704973ce443883beff0069e283c029bdb6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 16:48:16 -0400 Subject: [PATCH 044/451] folder threads and messages --- tests/test_folders.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_folders.py b/tests/test_folders.py index 1645e98f..50daf922 100644 --- a/tests/test_folders.py +++ b/tests/test_folders.py @@ -1,6 +1,6 @@ import pytest import responses -from nylas.client.restful_models import Folder +from nylas.client.restful_models import Folder, Thread, Message @responses.activate @@ -13,3 +13,23 @@ def test_get_change_folder(api_client): folder.display_name = 'My New Folder' folder.save() assert folder.display_name == 'My New Folder' + + +@responses.activate +@pytest.mark.xfail +@pytest.mark.usefixtures("mock_folder", "mock_threads") +def test_folder_threads(api_client): + folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') + assert folder.threads + assert all(isinstance(thread, Thread) + for thread in folder.threads) + + +@responses.activate +@pytest.mark.xfail +@pytest.mark.usefixtures("mock_folder", "mock_messages") +def test_folder_messages(api_client): + folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') + assert folder.messages + assert all(isinstance(message, Message) + for message in folder.messages) From 9e5299859ec83a64156a46b95faf9fd108b8ea2a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 16:52:07 -0400 Subject: [PATCH 045/451] Fix typo --- tests/test_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_messages.py b/tests/test_messages.py index 94b4c6cf..1472a449 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -34,7 +34,7 @@ def test_message_read(api_client): assert message.unread is False message.mark_as_unread() assert message.unread is True - # mark_as_seen() is a synonum for mark_as_read() + # mark_as_seen() is a synonym for mark_as_read() message.mark_as_seen() assert message.unread is False From 7089827bed791ad452871c8844b07455402bb8a9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 17:18:45 -0400 Subject: [PATCH 046/451] thread labels --- tests/conftest.py | 72 +++++++++++++++++++++++++++++++++++++++++++ tests/test_threads.py | 20 +++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 23733078..e48a24c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import re import json +import copy import pytest import responses import httpretty @@ -352,6 +353,77 @@ def request_callback(request): ) +@pytest.fixture +def mock_labelled_thread(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": [ + { + "display_name": "Important", + "id": "anuep8pe5ugmxrucchrzba2o8", + "name": "important", + "account_id": account_id, + "object": "label" + }, { + "display_name": "Existing", + "id": "dfslhgy3rlijfhlsujnchefs3", + "name": "existing", + "account_id": account_id, + "object": "label" + } + ] + } + 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') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + responses.add_callback( + responses.PUT, + endpoint, + content_type='application/json', + callback=request_callback, + ) + + @pytest.fixture def mock_drafts(api_url): response_body = json.dumps([{ diff --git a/tests/test_threads.py b/tests/test_threads.py index a7e449bc..5726fe79 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,6 +1,6 @@ import pytest import responses -from nylas.client.restful_models import Message, Draft +from nylas.client.restful_models import Message, Draft, Label @responses.activate @@ -44,3 +44,21 @@ def test_thread_drafts(api_client): assert thread.drafts assert all(isinstance(draft, Draft) for draft in thread.drafts) + + +@responses.activate +@pytest.mark.usefixtures("mock_labelled_thread", "mock_labels") +def test_thread_labels(api_client): + thread = api_client.threads.find(111) + assert len(thread.labels) == 2 + assert all(isinstance(label, Label) + for label in thread.labels) + + returned = thread.add_labels(["fake1", "fake2"]) + assert len(thread.labels) == 4 + assert thread.labels == returned + + label_ids = [l.id for l in thread.labels] + returned = thread.remove_labels(label_ids) + assert len(thread.labels) == 0 # pylint: disable=len-as-condition + assert thread.labels == returned From 3b9116070ae638b977a074bec6342a3abb0bb3ba Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 17:26:31 -0400 Subject: [PATCH 047/451] Install test dependencies separately from running tests on Travis --- .travis.yml | 2 +- setup.py | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65c8a6d9..57c7e83b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - "3.6" install: - "travis_retry pip install -U pytest" - - "python setup.py install" + - "travis_retry pip install .[test]" - "travis_retry pip install codecov" script: "python setup.py test --lint" after_success: diff --git a/setup.py b/setup.py index 88d009d7..223dffcf 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,21 @@ VERSION = re.search(r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) +run_dependencies = [ + "requests>=2.4.2", + "six>=1.4.1", + "bumpversion>=0.5.0", + "pyOpenSSL", # needed for SNI support, required by api.nylas.com + "ndg-httpsclient", + "pyasn1", +] +test_dependencies = [ + "pytest", + "pytest-cov", + "pytest-pylint", + "responses", + "httpretty" +] class PyTest(TestCommand): @@ -60,20 +75,10 @@ def main(): name="nylas", version=VERSION, packages=find_packages(), - - install_requires=[ - "requests>=2.4.2", - "six>=1.4.1", - "bumpversion>=0.5.0", - # needed for SNI support, required by api.nylas.com - "pyOpenSSL", - "ndg-httpsclient", - "pyasn1", - ], + install_requires=run_dependencies, dependency_links=[], - tests_require=[ - "pytest", "pytest-cov", "pytest-pylint", "responses", "httpretty" - ], + tests_require=test_dependencies, + extras_require={'test': test_dependencies}, cmdclass={'test': PyTest}, author="Nylas Team", author_email="support@nylas.com", From c5e881d9c02995ee7da039445da5988f39aef940 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 17:29:45 -0400 Subject: [PATCH 048/451] upgrade pip on travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 57c7e83b..2a00db7c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - "2.7" - "3.6" install: - - "travis_retry pip install -U pytest" + - "travis_retry pip install -U pip pytest" - "travis_retry pip install .[test]" - "travis_retry pip install codecov" script: "python setup.py test --lint" From ce4ded5534ef30f93f301930fff57d32fd800474 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 6 Jul 2017 17:36:34 -0400 Subject: [PATCH 049/451] ignore coverage for debugging lines --- nylas/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 81a94fd0..5b77075f 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -41,7 +41,7 @@ def _validate(response): status_code = response.status_code data = request.body - if DEBUG: + if DEBUG: # pragma: no cover print("{} {} ({}) => {}: {}".format(request.method, url, data, status_code, response.text)) From 31bad037fcfa58356aa3239a4a51437239b5ea15 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Fri, 7 Jul 2017 10:46:08 -0700 Subject: [PATCH 050/451] update cla link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 996a4fe2..38c1066b 100644 --- a/README.md +++ b/README.md @@ -377,7 +377,7 @@ print(client.messages.first().body) We'd love your help making Nylas better. We hang out on Slack. [Join the channel here ![Slack Invite Button](http://slack-invite.nylas.com/badge.svg)](http://slack-invite.nylas.com) You can also email [support@nylas.com](mailto:support@nylas.com). -Please sign the [Contributor License Agreement](https://nylas.com/cla.html) before submitting pull requests. (It's similar to other projects, like NodeJS or Meteor.) +Please sign the [Contributor License Agreement](https://goo.gl/forms/lKbET6S6iWsGoBbz2) before submitting pull requests. (It's similar to other projects, like NodeJS or Meteor.) ### Releasing a new version From 6bd0dee63c89fbfaef28de6933913a2f55267ff6 Mon Sep 17 00:00:00 2001 From: Michael Pfister Date: Fri, 7 Jul 2017 11:28:15 -0700 Subject: [PATCH 051/451] update slack invite link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 38c1066b..9bdabf72 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,7 @@ print(client.messages.first().body) ## Contributing -We'd love your help making Nylas better. We hang out on Slack. [Join the channel here ![Slack Invite Button](http://slack-invite.nylas.com/badge.svg)](http://slack-invite.nylas.com) You can also email [support@nylas.com](mailto:support@nylas.com). +We'd love your help making Nylas better. We hang out on Slack. [Join the channel here.](http://slack-invite.nylas.com) You can also email [support@nylas.com](mailto:support@nylas.com). Please sign the [Contributor License Agreement](https://goo.gl/forms/lKbET6S6iWsGoBbz2) before submitting pull requests. (It's similar to other projects, like NodeJS or Meteor.) From 65b18b1aa9d661d2b7607bbc82a3453af0442d6d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:29:53 -0400 Subject: [PATCH 052/451] Thread read/unread --- tests/test_threads.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_threads.py b/tests/test_threads.py index 5726fe79..39ff2ac5 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -62,3 +62,19 @@ def test_thread_labels(api_client): returned = thread.remove_labels(label_ids) assert len(thread.labels) == 0 # pylint: disable=len-as-condition assert thread.labels == returned + + +@responses.activate +@pytest.mark.usefixtures("mock_threads", "mock_thread") +def test_thread_read(api_client): + thread = api_client.threads.first() + assert thread.unread is False + thread.mark_as_unread() + assert thread.unread is True + thread.mark_as_read() + assert thread.unread is False + # mark_as_seen() is a synonym for mark_as_read() + thread.mark_as_unread() + assert thread.unread is True + thread.mark_as_seen() + assert thread.unread is False From 12d6227aac002be93a111bdba928620069d6738d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:32:00 -0400 Subject: [PATCH 053/451] Thread add/remove single label --- tests/test_threads.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_threads.py b/tests/test_threads.py index 39ff2ac5..58ef8321 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -46,6 +46,23 @@ def test_thread_drafts(api_client): for draft in thread.drafts) +@responses.activate +@pytest.mark.usefixtures("mock_labelled_thread", "mock_labels") +def test_thread_label(api_client): + thread = api_client.threads.find(111) + assert len(thread.labels) == 2 + assert all(isinstance(label, Label) + for label in thread.labels) + + returned = thread.add_label("fake1") + assert len(thread.labels) == 3 + assert thread.labels == returned + + returned = thread.remove_label("fake1") + assert len(thread.labels) == 2 # pylint: disable=len-as-condition + assert thread.labels == returned + + @responses.activate @pytest.mark.usefixtures("mock_labelled_thread", "mock_labels") def test_thread_labels(api_client): From 5ea196df8d19e3fed5b4b3024c945e22972d8844 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:32:43 -0400 Subject: [PATCH 054/451] thread star --- tests/test_threads.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_threads.py b/tests/test_threads.py index 58ef8321..ba0ba61a 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -22,6 +22,8 @@ def test_thread_change(api_client): assert thread.starred thread.unstar() assert not thread.starred + thread.star() + assert thread.starred thread.update_folder('qwer') assert len(thread.folders) == 1 From 6c46ace675f06597a9c465072c1f15560933edf1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:34:15 -0400 Subject: [PATCH 055/451] draft create reply --- tests/test_threads.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_threads.py b/tests/test_threads.py index ba0ba61a..1b75eca2 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -97,3 +97,13 @@ def test_thread_read(api_client): assert thread.unread is True thread.mark_as_seen() assert thread.unread is False + + +@responses.activate +@pytest.mark.usefixtures("mock_threads") +def test_thread_reply(api_client): + thread = api_client.threads.first() + draft = thread.create_reply() + assert isinstance(draft, Draft) + assert draft.thread_id == thread.id + assert draft.subject == thread.subject From 6665c9d7beb86f92c0a21779e4b70a3fe2f5447a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:35:15 -0400 Subject: [PATCH 056/451] pytest.raises() --- tests/test_drafts.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 7aeb00f7..df6cc0a6 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -23,10 +23,5 @@ def test_save_send_draft(api_client): assert msg['thread_id'] == 'clm33kapdxkposgltof845v9s' # Second time should throw an error - raised = False - try: + with pytest.raises(InvalidRequestError): draft.send() - except InvalidRequestError: - raised = True - - assert raised is True From edba209524d92d36556d2a1d10548af9a9fcba25 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 09:50:03 -0400 Subject: [PATCH 057/451] draft attachments --- tests/conftest.py | 19 +++++++++++++++++++ tests/test_drafts.py | 24 ++++++++++++++++++++++++ tests/test_events.py | 4 ---- tests/test_files.py | 18 ++---------------- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e48a24c8..2bc276c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -601,8 +601,26 @@ def callback(request): ) +@pytest.fixture +def mock_files(api_url): + httpretty.enable() + body = [{ + "content_type": "text/plain", + "filename": "a.txt", + "id": "3qfe4k3siosfjtjpfdnon8zbn", + "account_id": "6aakaxzi4j5gn6f7kbb9e0fxs", + "object": "file", + "size": 762878 + }] + + values = [httpretty.Response(status=200, body=json.dumps(body))] + httpretty.register_uri(httpretty.POST, api_url + '/files/', responses=values) + httpretty.register_uri(httpretty.GET, api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', + body='test body') + @pytest.fixture def mock_event_create_response(api_url, message_body): + httpretty.enable() values = [ httpretty.Response(status=200, body=json.dumps(message_body)), httpretty.Response(status=400, body=''), @@ -623,6 +641,7 @@ def mock_event_create_response(api_url, message_body): @pytest.fixture def mock_event_create_notify_response(api_url, message_body): + httpretty.enable() httpretty.register_uri( httpretty.POST, api_url + '/events/?notify_participants=true&other_param=1', diff --git a/tests/test_drafts.py b/tests/test_drafts.py index df6cc0a6..3d556edb 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -25,3 +25,27 @@ def test_save_send_draft(api_client): # Second time should throw an error with pytest.raises(InvalidRequestError): draft.send() + + +@pytest.mark.usefixtures("mock_files") +def test_draft_attachment(api_client): + draft = api_client.drafts.create() + attachment = api_client.files.create() + attachment.filename = "dummy" + attachment.data = "data" + + assert len(draft.file_ids) == 0 + draft.attach(attachment) + assert len(draft.file_ids) == 1 + assert attachment.id in draft.file_ids + + unattached = api_client.files.create() + unattached.filename = "unattached" + unattached.data = "foo" + draft.detach(unattached) + assert len(draft.file_ids) == 1 + assert attachment.id in draft.file_ids + assert unattached.id not in draft.file_ids + + draft.detach(attachment) + assert len(draft.file_ids) == 0 diff --git a/tests/test_events.py b/tests/test_events.py index 095d8125..3e99e217 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -13,8 +13,6 @@ def blank_event(api_client): @pytest.mark.usefixtures("mock_event_create_response") def test_event_crud(api_client): - httpretty.enable() - event1 = blank_event(api_client) event1.save() assert event1.id == 'cv4ei7syx10uvsxbs21ccsezf' @@ -34,8 +32,6 @@ def test_event_crud(api_client): @pytest.mark.usefixtures("mock_event_create_notify_response") def test_event_notify(api_client): - httpretty.enable() - event1 = blank_event(api_client) event1.save(notify_participants='true', other_param='1') assert event1.id == 'cv4ei7syx10uvsxbs21ccsezf' diff --git a/tests/test_files.py b/tests/test_files.py index 204250bb..ed457030 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -5,22 +5,8 @@ from nylas.client.errors import FileUploadError -def test_file_upload(api_client, api_url): - httpretty.enable() - body = [{ - "content_type": "text/plain", - "filename": "a.txt", - "id": "3qfe4k3siosfjtjpfdnon8zbn", - "account_id": "6aakaxzi4j5gn6f7kbb9e0fxs", - "object": "file", - "size": 762878 - }] - - values = [Response(status=200, body=json.dumps(body))] - httpretty.register_uri(httpretty.POST, api_url + '/files/', responses=values) - httpretty.register_uri(httpretty.GET, api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', - body='test body') - +@pytest.mark.usefixtures("mock_files") +def test_file_upload(api_client): myfile = api_client.files.create() myfile.filename = 'test.txt' myfile.data = "Hello World." From 4ad1c100afd8dd8d41f424b84fcaf1f07c6be937 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:00:39 -0400 Subject: [PATCH 058/451] delete draft --- tests/conftest.py | 11 +++++++++++ tests/test_drafts.py | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2bc276c7..c6b4629e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -554,6 +554,17 @@ def mock_draft_updated_response(api_url): ) +@pytest.fixture +def mock_draft_deleted_response(api_url): + responses.add( + responses.DELETE, + api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', + content_type='application/json', + status=200, + body="", + ) + + @pytest.fixture def mock_draft_sent_response(api_url): body = { diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 3d556edb..ca28787a 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -49,3 +49,19 @@ def test_draft_attachment(api_client): draft.detach(attachment) assert len(draft.file_ids) == 0 + + +@responses.activate +@pytest.mark.usefixtures( + "mock_draft_saved_response", "mock_draft_deleted_response" +) +def test_delete_draft(api_client): + draft = api_client.drafts.create() + # Unsaved draft shouldn't throw an error on .delete(), but won't actually + # delete anything. + draft.delete() + # Now save the draft, and update the version so it's truthy + draft.save() + draft.version = 1 + # Delete it for real. + draft.delete() From 8125a9f6ba0ebd64b313841f63adb48e4e3df4a8 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:02:51 -0400 Subject: [PATCH 059/451] invalid file upload --- tests/test_files.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_files.py b/tests/test_files.py index ed457030..c62adf5c 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -19,6 +19,17 @@ def test_file_upload(api_client): assert data == 'test body' +def test_file_invalid_upload(api_client): + myfile = api_client.files.create() + with pytest.raises(FileUploadError) as exc: + myfile.save() + + assert exc.value.message == ( + "File object not properly formatted, " + "must provide either a stream or data." + ) + + def test_file_upload_errors(api_client): myfile = api_client.files.create() myfile.filename = 'test.txt' From 5e6d79b9c1f22554b5f784b108d15c35727919ea Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:03:41 -0400 Subject: [PATCH 060/451] Remove httpretty disable --- tests/test_events.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_events.py b/tests/test_events.py index 3e99e217..6e0e4f98 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -27,8 +27,6 @@ def test_event_crud(api_client): with pytest.raises(InvalidRequestError): event2.save() - httpretty.disable() - @pytest.mark.usefixtures("mock_event_create_notify_response") def test_event_notify(api_client): @@ -39,5 +37,3 @@ def test_event_notify(api_client): query = httpretty.last_request().querystring assert query['notify_participants'][0] == 'true' assert query['other_param'][0] == '1' - - httpretty.disable() From 3e72f040a85f4ec9510b4253ebe46388f5bc69c6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:17:20 -0400 Subject: [PATCH 061/451] calendar events --- tests/conftest.py | 53 +++++++++++++++++++++++++++++++++++++++++++- tests/test_events.py | 11 +++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index c6b4629e..e50a32e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -213,7 +213,6 @@ def request_callback(request): ) - @pytest.fixture def mock_messages(api_url, account_id): response_body = json.dumps([ @@ -800,3 +799,55 @@ def mock_message_search_response(api_url): content_type='application/json', match_querystring=True ) + + +@pytest.fixture +def mock_calendars(api_url): + response_body = json.dumps([ + { + "id": "8765", + "events": [ + { + "title": "Pool party", + "location": "Local Community Pool", + "participants": [ + "Alice", + "Bob", + "Claire", + "Dot", + ] + } + ], + } + ]) + endpoint = re.compile(api_url + '/calendars') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) + +@pytest.fixture +def mock_events(api_url): + response_body = json.dumps([ + { + "title": "Pool party", + "location": "Local Community Pool", + "participants": [ + "Alice", + "Bob", + "Claire", + "Dot", + ] + } + ]) + endpoint = re.compile(api_url + '/events') + responses.add( + responses.GET, + endpoint, + content_type='application/json', + status=200, + body=response_body + ) diff --git a/tests/test_events.py b/tests/test_events.py index 6e0e4f98..30b89a77 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,6 +1,8 @@ import pytest import httpretty +import responses from nylas.client.errors import InvalidRequestError +from nylas.client.restful_models import Event def blank_event(api_client): @@ -37,3 +39,12 @@ def test_event_notify(api_client): query = httpretty.last_request().querystring assert query['notify_participants'][0] == 'true' assert query['other_param'][0] == '1' + + +@responses.activate +@pytest.mark.usefixtures("mock_calendars", "mock_events") +def test_calendar_events(api_client): + calendar = api_client.calendars.first() + assert calendar.events + assert all(isinstance(event, Event) + for event in calendar.events) From 26b50cec0c39750ee8508f9c84dbca68c62fb9e4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:26:51 -0400 Subject: [PATCH 062/451] Basic account creation tests --- tests/test_accounts.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_accounts.py diff --git a/tests/test_accounts.py b/tests/test_accounts.py new file mode 100644 index 00000000..774bb75e --- /dev/null +++ b/tests/test_accounts.py @@ -0,0 +1,14 @@ +import pytest +from nylas.client.restful_models import Account, APIAccount + + +def test_create_account(api_client, monkeypatch): + monkeypatch.setattr(api_client, "is_opensource_api", lambda: False) + account = api_client.accounts.create() + assert isinstance(account, Account) + + +def test_create_apiaccount(api_client, monkeypatch): + monkeypatch.setattr(api_client, "is_opensource_api", lambda: True) + account = api_client.accounts.create() + assert isinstance(account, APIAccount) From e166cd4433280a2506ab7bcef24613e65ff9c975 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:30:06 -0400 Subject: [PATCH 063/451] Fix pylint errors --- setup.py | 10 +++++----- tests/test_accounts.py | 1 - tests/test_drafts.py | 2 ++ tests/test_files.py | 3 --- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 223dffcf..487a8c1d 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ VERSION = re.search(r'^__VERSION__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE).group(1) -run_dependencies = [ +RUN_DEPENDENCIES = [ "requests>=2.4.2", "six>=1.4.1", "bumpversion>=0.5.0", @@ -18,7 +18,7 @@ "ndg-httpsclient", "pyasn1", ] -test_dependencies = [ +TEST_DEPENDENCIES = [ "pytest", "pytest-cov", "pytest-pylint", @@ -75,10 +75,10 @@ def main(): name="nylas", version=VERSION, packages=find_packages(), - install_requires=run_dependencies, + install_requires=RUN_DEPENDENCIES, dependency_links=[], - tests_require=test_dependencies, - extras_require={'test': test_dependencies}, + tests_require=TEST_DEPENDENCIES, + extras_require={'test': TEST_DEPENDENCIES}, cmdclass={'test': PyTest}, author="Nylas Team", author_email="support@nylas.com", diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 774bb75e..819896f3 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,4 +1,3 @@ -import pytest from nylas.client.restful_models import Account, APIAccount diff --git a/tests/test_drafts.py b/tests/test_drafts.py index ca28787a..5070153e 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -2,6 +2,8 @@ import responses from nylas.client.errors import InvalidRequestError +# pylint: disable=len-as-condition + @responses.activate @pytest.mark.usefixtures( diff --git a/tests/test_files.py b/tests/test_files.py index c62adf5c..046c274d 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,7 +1,4 @@ -import json import pytest -import httpretty -from httpretty import Response from nylas.client.errors import FileUploadError From 8dc46faa9b63c1f74ded99dc4d44ce39ca05e579 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 10:45:17 -0400 Subject: [PATCH 064/451] More account tests --- tests/test_accounts.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 819896f3..10b6597d 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,3 +1,4 @@ +import pytest from nylas.client.restful_models import Account, APIAccount @@ -11,3 +12,28 @@ def test_create_apiaccount(api_client, monkeypatch): monkeypatch.setattr(api_client, "is_opensource_api", lambda: True) account = api_client.accounts.create() assert isinstance(account, APIAccount) + + +def test_account_json(api_client, monkeypatch): + monkeypatch.setattr(api_client, "is_opensource_api", lambda: False) + account = api_client.accounts.create() + result = account.as_json() + assert isinstance(result, dict) + + +@pytest.mark.xfail +def test_account_upgrade(api_client, monkeypatch): + monkeypatch.setattr(api_client, "is_opensource_api", lambda: False) + account = api_client.accounts.create() + assert account.billing_state is False # what should this be? + account.upgrade() + assert account.billing_state is True + account.downgrade() + assert account.billing_state is False + + +def test_account_delete(api_client, monkeypatch): + monkeypatch.setattr(api_client, "is_opensource_api", lambda: False) + account = api_client.accounts.create() + with pytest.raises(NotImplementedError): + account.delete() From b4254abcf01e5a1c4cc7b655ecbadc5c75abb225 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 15:10:18 -0400 Subject: [PATCH 065/451] Don't treat the draft version as a boolean --- nylas/client/restful_models.py | 2 +- tests/test_drafts.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 701c79d8..5e2c10b5 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -343,7 +343,7 @@ def send(self): return msg def delete(self): - if self.id and self.version: + if self.id and self.version is not None: data = {'version': self.version} self.api._delete_resource(self.cls, self.id, data=data) diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 5070153e..7b945c0a 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -62,8 +62,7 @@ def test_delete_draft(api_client): # Unsaved draft shouldn't throw an error on .delete(), but won't actually # delete anything. draft.delete() - # Now save the draft, and update the version so it's truthy + # Now save the draft... draft.save() - draft.version = 1 - # Delete it for real. + # ... and delete it for real. draft.delete() From 704332774091ac7a0461d9dce0f96a6551f4522f Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 16:06:06 -0400 Subject: [PATCH 066/451] tests for client functionality --- setup.py | 3 +- tests/test_client.py | 121 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 tests/test_client.py diff --git a/setup.py b/setup.py index 487a8c1d..4dd8524e 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,8 @@ "pytest-cov", "pytest-pylint", "responses", - "httpretty" + "httpretty", + "urlobject", ] diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..52b2f0d8 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,121 @@ +import re +import json +from six.moves.urllib.parse import parse_qs +import pytest +from urlobject import URLObject +import responses +from nylas.client import APIClient + + +def urls_equal(url1, url2): + """ + Compare two URLObjects, without regard to the order of their query strings. + """ + return ( + url1.without_query() == url2.without_query() and + url1.query_dict == url2.query_dict + ) + + +def test_custom_client(): + # Can specify API server + custom = APIClient(api_server="http://example.com") + assert custom.api_server == "http://example.com" + # Must be a valid URL + with pytest.raises(Exception) as exc: + APIClient(api_server="invalid") + assert exc.value.message == ( + "When overriding the Nylas API server address, " + "you must include https://" + ) + + +def test_client_access_token(): + client = APIClient(access_token="foo") + assert client.access_token == "foo" + assert client.session.headers['Authorization'] == "Bearer foo" + client.access_token = "bar" + assert client.access_token == "bar" + assert client.session.headers['Authorization'] == "Bearer bar" + client.access_token = None + assert client.access_token is None + assert 'Authorization' not in client.session.headers + + +def test_client_app_secret(): + client = APIClient(app_secret="foo") + headers = client.admin_session.headers + assert headers['Authorization'] == "Basic Zm9vOg==" + assert headers['X-Nylas-API-Wrapper'] == "python" + assert "Nylas Python SDK" in headers['User-Agent'] + + +def test_client_authentication_url(api_client, api_url): + expected = ( + URLObject(api_url) + .with_path("/oauth/authorize") + .set_query_params([ + ('login_hint', ''), + ('state', ''), + ('redirect_uri', '/redirect'), + ('response_type', 'code'), + ('client_id', 'None'), + ('scope', 'email'), + ]) + ) + actual = URLObject(api_client.authentication_url("/redirect")) + assert urls_equal(expected, actual) + + actual2 = URLObject( + api_client.authentication_url("/redirect", login_hint="hint") + ) + expected2 = expected.set_query_param("login_hint", "hint") + assert urls_equal(expected2, actual2) + + +@responses.activate +def test_client_token_for_code(api_client, api_url): + endpoint = re.compile(api_url + '/oauth/token') + response_body = json.dumps({"access_token": "hooray"}) + responses.add( + responses.POST, + endpoint, + content_type='application/json', + status=200, + body=response_body, + ) + + assert api_client.token_for_code("foo") == "hooray" + assert len(responses.calls) == 1 + request = responses.calls[0].request + body = parse_qs(request.body) + assert body["grant_type"] == ["authorization_code"] + assert body["code"] == ["foo"] + + +def test_client_opensource_api(api_client): + # pylint: disable=singleton-comparison + assert api_client.is_opensource_api() == True + api_client.app_id = "foo" + api_client.app_secret = "super-sekrit" + assert api_client.is_opensource_api() == False + api_client.app_id = api_client.app_secret = None + assert api_client.is_opensource_api() == True + + +@responses.activate +def test_client_revoke_token(api_client, api_url): + endpoint = re.compile(api_url + '/oauth/revoke') + responses.add( + responses.POST, + endpoint, + status=200, + body="", + ) + + api_client.auth_token = "foo" + api_client.access_token = "bar" + api_client.revoke_token() + assert api_client.auth_token is None + assert api_client.access_token is None + assert len(responses.calls) == 1 From fed741393585d8c674d83ebb9af342dd1ba121c0 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 16:20:31 -0400 Subject: [PATCH 067/451] py3 compat --- nylas/client/client.py | 7 +++++-- tests/test_client.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 5b77075f..2374f199 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -118,9 +118,12 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), self.admin_session = requests.Session() if app_secret is not None: - b64_app_secret = b64encode(app_secret + ':') + b64_app_secret = b64encode((app_secret + ':').encode('utf8')) + authorization = 'Basic {secret}'.format( + secret=b64_app_secret.decode('utf8') + ) self.admin_session.headers = { - 'Authorization': 'Basic {secret}'.format(secret=b64_app_secret), + 'Authorization': authorization, 'X-Nylas-API-Wrapper': 'python', 'User-Agent': version_header, } diff --git a/tests/test_client.py b/tests/test_client.py index 52b2f0d8..b80040fc 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,7 +24,7 @@ def test_custom_client(): # Must be a valid URL with pytest.raises(Exception) as exc: APIClient(api_server="invalid") - assert exc.value.message == ( + assert exc.value.args[0] == ( "When overriding the Nylas API server address, " "you must include https://" ) From 709be7cf0d4a604fc9707935c6ca1e470a064198 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 16:56:21 -0400 Subject: [PATCH 068/451] Add test for arbitrary kwargs in delete requests --- tests/conftest.py | 7 +++++++ tests/test_messages.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index e50a32e9..e4b0f607 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -282,6 +282,13 @@ def request_callback(request): content_type='application/json', callback=request_callback ) + responses.add( + responses.DELETE, + endpoint, + content_type='application/json', + status=200, + body="", + ) @pytest.fixture diff --git a/tests/test_messages.py b/tests/test_messages.py index 1472a449..c501a2ee 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -2,6 +2,7 @@ import six import pytest import responses +from urlobject import URLObject @responses.activate @@ -75,3 +76,13 @@ def test_message_raw(api_client, account_id): "id": "1234", "subject": "Test Message" }] + + +@responses.activate +@pytest.mark.usefixtures("mock_message") +def test_message_delete_by_id(api_client): + api_client.messages.delete(1234, forceful=True) + assert len(responses.calls) == 1 + request = responses.calls[0].request + url = URLObject(request.url) + assert url.query_dict["forceful"] == "True" From 7698c658de74f7f84fccf4e6f5e191f86f403081 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 17:26:47 -0400 Subject: [PATCH 069/451] Fix tests for upgrade & downgrade --- nylas/client/client.py | 4 +- nylas/client/restful_models.py | 10 +++-- tests/conftest.py | 70 +++++++++++++++++++++++++++++++++- tests/test_accounts.py | 20 +++++----- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 2374f199..327ce782 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -383,8 +383,8 @@ def _call_resource_method(self, cls, id, method_name, data): method_name, ) - session = self._get_http_session(cls.api_root) response = session.post(url, json=data) - return _validate(response).json() + result = _validate(response).json() + return cls.create(self, **result) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 701c79d8..e2eb5288 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -454,12 +454,14 @@ def as_json(self): return dct def upgrade(self): - self.api._call_resource_method(self, self.account_id, - 'upgrade', None) + return self.api._call_resource_method( + self, self.account_id, 'upgrade', None + ) def downgrade(self): - self.api._call_resource_method(self, self.account_id, - 'downgrade', None) + return self.api._call_resource_method( + self, self.account_id, 'downgrade', None + ) def delete(self): raise NotImplementedError diff --git a/tests/conftest.py b/tests/conftest.py index e4b0f607..3f4e65e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,6 +42,11 @@ def account_id(): return '4ennivvrcgsqytgybfk912dto' +@pytest.fixture +def app_id(): + return 'fake-app-id' + + @pytest.fixture def api_client(api_url): return APIClient(None, None, None, api_url) @@ -74,7 +79,8 @@ def mock_account(api_url, account_id): "name": "Ben Bitdiddle", "object": "account", "provider": "gmail", - "organization_unit": "label" + "organization_unit": "label", + "billing_state": "trial", } ) responses.add( @@ -87,6 +93,30 @@ def mock_account(api_url, account_id): ) +@pytest.fixture +def mock_accounts(api_url, account_id, app_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": "trial", + } + ]) + url = "{base}/a/{app_id}/accounts".format(base=api_url, app_id=app_id) + responses.add( + responses.GET, + url, + content_type='application/json', + status=200, + body=response_body, + ) + + @pytest.fixture def mock_folder_account(api_url, account_id): response_body = json.dumps( @@ -858,3 +888,41 @@ def mock_events(api_url): status=200, body=response_body ) + + +@pytest.fixture +def mock_upgrade(api_url, account_id, app_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": "trial", + } + trial_response = json.dumps(account) + account["billing_state"] = "paid" + paid_response = json.dumps(account) + + upgrade_url = "{base}/a/{app_id}/accounts/{id}/upgrade".format( + base=api_url, id=account_id, app_id=app_id, + ) + downgrade_url = "{base}/a/{app_id}/accounts/{id}/downgrade".format( + base=api_url, id=account_id, app_id=app_id, + ) + responses.add( + responses.POST, + re.compile(upgrade_url), + content_type='application/json', + status=200, + body=paid_response, + ) + responses.add( + responses.POST, + re.compile(downgrade_url), + content_type='application/json', + status=200, + body=trial_response, + ) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 10b6597d..3bfa904b 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,4 +1,5 @@ import pytest +import responses from nylas.client.restful_models import Account, APIAccount @@ -21,15 +22,16 @@ def test_account_json(api_client, monkeypatch): assert isinstance(result, dict) -@pytest.mark.xfail -def test_account_upgrade(api_client, monkeypatch): - monkeypatch.setattr(api_client, "is_opensource_api", lambda: False) - account = api_client.accounts.create() - assert account.billing_state is False # what should this be? - account.upgrade() - assert account.billing_state is True - account.downgrade() - assert account.billing_state is False +@responses.activate +@pytest.mark.usefixtures("mock_accounts", "mock_upgrade") +def test_account_upgrade(api_client, app_id): + api_client.app_id = app_id + account = api_client.accounts.first() + assert account.billing_state == "trial" + account = account.upgrade() + assert account.billing_state == "paid" + account = account.downgrade() + assert account.billing_state == "trial" def test_account_delete(api_client, monkeypatch): From ec08eac5f0535893ebd41fe93800ede9c1e48c02 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 18:04:31 -0400 Subject: [PATCH 070/451] test for creating multiple contacts in one API call --- tests/test_client.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index b80040fc..0cbe0d85 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,6 +5,7 @@ from urlobject import URLObject import responses from nylas.client import APIClient +from nylas.client.restful_models import Contact def urls_equal(url1, url2): @@ -119,3 +120,34 @@ def test_client_revoke_token(api_client, api_url): assert api_client.auth_token is None assert api_client.access_token is None assert len(responses.calls) == 1 + + +@responses.activate +def test_create_resources(api_client, api_url): + contacts_data = [ + { + "id": 1, + "name": "first", + "email": "first@example.com", + }, { + "id": 2, + "name": "second", + "email": "second@example.com", + } + ] + responses.add( + responses.POST, + api_url + "/contacts/", + content_type='application/json', + status=200, + body=json.dumps(contacts_data), + ) + + post_data = contacts_data.copy() + for contact in post_data: + del contact["id"] + + contacts = api_client._create_resources(Contact, post_data) + assert len(contacts) == 2 + assert all(isinstance(contact, Contact) for contact in contacts) + assert len(responses.calls) == 1 From 16f8c47eb61d833d99daba6974d7470b255aff28 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 18:08:21 -0400 Subject: [PATCH 071/451] py2 compat --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0cbe0d85..aa14a52b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -143,7 +143,7 @@ def test_create_resources(api_client, api_url): body=json.dumps(contacts_data), ) - post_data = contacts_data.copy() + post_data = list(contacts_data) # make a copy for contact in post_data: del contact["id"] From a3a0849444cefa7f69156b0666601cae567cb7a4 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 18:13:56 -0400 Subject: [PATCH 072/451] billing_status is paid or cancelled --- tests/conftest.py | 18 +++++++++--------- tests/test_accounts.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3f4e65e9..7c02c97b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,7 +80,7 @@ def mock_account(api_url, account_id): "object": "account", "provider": "gmail", "organization_unit": "label", - "billing_state": "trial", + "billing_state": "paid", } ) responses.add( @@ -104,7 +104,7 @@ def mock_accounts(api_url, account_id, app_id): "object": "account", "provider": "gmail", "organization_unit": "label", - "billing_state": "trial", + "billing_state": "paid", } ]) url = "{base}/a/{app_id}/accounts".format(base=api_url, app_id=app_id) @@ -891,7 +891,7 @@ def mock_events(api_url): @pytest.fixture -def mock_upgrade(api_url, account_id, app_id): +def mock_account_management(api_url, account_id, app_id): account = { "account_id": account_id, "email_address": "ben.bitdiddle1861@gmail.com", @@ -900,11 +900,11 @@ def mock_upgrade(api_url, account_id, app_id): "object": "account", "provider": "gmail", "organization_unit": "label", - "billing_state": "trial", + "billing_state": "paid", } - trial_response = json.dumps(account) - account["billing_state"] = "paid" paid_response = json.dumps(account) + account["billing_state"] = "cancelled" + cancelled_response = json.dumps(account) upgrade_url = "{base}/a/{app_id}/accounts/{id}/upgrade".format( base=api_url, id=account_id, app_id=app_id, @@ -914,15 +914,15 @@ def mock_upgrade(api_url, account_id, app_id): ) responses.add( responses.POST, - re.compile(upgrade_url), + upgrade_url, content_type='application/json', status=200, body=paid_response, ) responses.add( responses.POST, - re.compile(downgrade_url), + downgrade_url, content_type='application/json', status=200, - body=trial_response, + body=cancelled_response, ) diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 3bfa904b..4c34afed 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -23,15 +23,15 @@ def test_account_json(api_client, monkeypatch): @responses.activate -@pytest.mark.usefixtures("mock_accounts", "mock_upgrade") +@pytest.mark.usefixtures("mock_accounts", "mock_account_management") def test_account_upgrade(api_client, app_id): api_client.app_id = app_id account = api_client.accounts.first() - assert account.billing_state == "trial" - account = account.upgrade() assert account.billing_state == "paid" account = account.downgrade() - assert account.billing_state == "trial" + assert account.billing_state == "cancelled" + account = account.upgrade() + assert account.billing_state == "paid" def test_account_delete(api_client, monkeypatch): From bd486b979098052c9f0f0bfdef7c47da9d0ca01a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 18:24:18 -0400 Subject: [PATCH 073/451] Tests for APIClientError --- tests/test_client.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index aa14a52b..5b8ab13c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,6 +5,7 @@ from urlobject import URLObject import responses from nylas.client import APIClient +from nylas.client.errors import APIClientError from nylas.client.restful_models import Contact @@ -31,6 +32,15 @@ def test_custom_client(): ) +def test_client_error(): + err1 = APIClientError(message="this is a message") + assert err1.args[0] == "this is a message" + assert str(err1) == '{"message": "this is a message"}' + err2 = APIClientError(something="this is unusual") + assert err2.args[0] == "" + assert str(err2) == '{"something": "this is unusual"}' + + def test_client_access_token(): client = APIClient(access_token="foo") assert client.access_token == "foo" From 0e7f98b2f9f50198de12ab5233978efa77b5746c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 18:39:15 -0400 Subject: [PATCH 074/451] Switch to using urlobject library --- nylas/client/client.py | 11 ++++++----- nylas/client/util.py | 30 ------------------------------ setup.py | 2 +- tests/test_client.py | 2 +- 4 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 nylas/client/util.py diff --git a/nylas/client/client.py b/nylas/client/client.py index 327ce782..be06d27f 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -4,9 +4,9 @@ from os import environ from base64 import b64encode import requests +from urlobject import URLObject from six.moves.urllib.parse import urlencode from nylas._client_sdk_version import __VERSION__ -from nylas.client.util import url_concat from nylas.client.restful_model_collection import RestfulModelCollection from nylas.client.restful_models import ( Calendar, Contact, Event, Message, Thread, File, @@ -145,13 +145,14 @@ def access_token(self, value): def authentication_url(self, redirect_uri, login_hint=''): args = {'redirect_uri': redirect_uri, - 'client_id': self.app_id, + 'client_id': self.app_id or '', 'response_type': 'code', 'scope': 'email', 'login_hint': login_hint, 'state': ''} - return url_concat(self.authorize_url, args) + url = URLObject(self.authorize_url).add_query_params(args.items()) + return str(url) def token_for_code(self, code): args = {'client_id': self.app_id, @@ -258,7 +259,7 @@ def _get_resources(self, cls, extra=None, **filters): postfix ) - url = url_concat(url, filters) + url = str(URLObject(url).add_query_params(filters.items())) response = self._get_http_session(cls.api_root).get(url) results = _validate(response).json() return [ @@ -282,7 +283,7 @@ def _get_resource_raw(self, cls, id, extra=None, url = "{}/a/{}/{}/{}{}".format(self.api_server, self.app_id, cls.collection_name, id, postfix) - url = url_concat(url, filters) + url = str(URLObject(url).add_query_params(filters.items())) response = self._get_http_session(cls.api_root).get(url, headers=headers) return _validate(response) diff --git a/nylas/client/util.py b/nylas/client/util.py deleted file mode 100644 index 283a23ff..00000000 --- a/nylas/client/util.py +++ /dev/null @@ -1,30 +0,0 @@ -from six.moves.urllib.parse import urlencode - - -# From tornado.httputil -def url_concat(url, args, fragments=None): - """Concatenate url and argument dictionary regardless of whether - url has existing query parameters. - - >>> url_concat("http://example.com/foo?a=b", dict(c="d")) - 'http://example.com/foo?a=b&c=d' - """ - - if not args and not fragments: - return url - - # Strip off hashes - while url[-1] == '#': - url = url[:-1] - - fragment_tail = '' - if fragments: - fragment_tail = '#' + urlencode(fragments) - - args_tail = '' - if args: - if url[-1] not in ('?', '&'): - args_tail += '&' if ('?' in url) else '?' - args_tail += urlencode(args) - - return url + args_tail + fragment_tail diff --git a/setup.py b/setup.py index 4dd8524e..9cc37caf 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ "pyOpenSSL", # needed for SNI support, required by api.nylas.com "ndg-httpsclient", "pyasn1", + "urlobject", ] TEST_DEPENDENCIES = [ "pytest", @@ -24,7 +25,6 @@ "pytest-pylint", "responses", "httpretty", - "urlobject", ] diff --git a/tests/test_client.py b/tests/test_client.py index aa14a52b..27db8ff7 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,7 +60,7 @@ def test_client_authentication_url(api_client, api_url): ('state', ''), ('redirect_uri', '/redirect'), ('response_type', 'code'), - ('client_id', 'None'), + ('client_id', ''), ('scope', 'email'), ]) ) From 09ec57857e2a5ecd7e4625c08bd223cf8bb53fc8 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 19:09:32 -0400 Subject: [PATCH 075/451] Test different ways to access account objects --- nylas/client/restful_models.py | 2 +- tests/conftest.py | 7 +++---- tests/test_accounts.py | 14 +++++++++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 7d1822f7..93488a0d 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -471,7 +471,7 @@ class APIAccount(NylasAPIObject): attrs = ['account_id', 'email_address', 'id', 'name', 'object', 'organization_unit', 'provider', 'sync_state'] - collection_name = 'account' + collection_name = 'accounts' def __init__(self, api): NylasAPIObject.__init__(self, APIAccount, api) diff --git a/tests/conftest.py b/tests/conftest.py index 7c02c97b..2fe8fad2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,11 +85,10 @@ def mock_account(api_url, account_id): ) responses.add( responses.GET, - api_url + '/account', + re.compile(api_url + '/account/?'), content_type='application/json', status=200, body=response_body, - match_querystring=True ) @@ -107,10 +106,10 @@ def mock_accounts(api_url, account_id, app_id): "billing_state": "paid", } ]) - url = "{base}/a/{app_id}/accounts".format(base=api_url, app_id=app_id) + url_re = "{base}(/a/{app_id})?/accounts/?".format(base=api_url, app_id=app_id) responses.add( responses.GET, - url, + re.compile(url_re), content_type='application/json', status=200, body=response_body, diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 4c34afed..6714ef8b 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,6 +1,6 @@ import pytest import responses -from nylas.client.restful_models import Account, APIAccount +from nylas.client.restful_models import Account, APIAccount, SingletonAccount def test_create_account(api_client, monkeypatch): @@ -39,3 +39,15 @@ def test_account_delete(api_client, monkeypatch): account = api_client.accounts.create() with pytest.raises(NotImplementedError): account.delete() + + +@responses.activate +@pytest.mark.usefixtures("mock_accounts", "mock_account") +def test_account_access(api_client): + account1 = api_client.account + assert isinstance(account1, SingletonAccount) + account2 = api_client.accounts[0] + assert isinstance(account2, APIAccount) + account3 = api_client.accounts.first() + assert isinstance(account3, APIAccount) + assert account1.as_json() == account2.as_json() == account3.as_json() From 118c564fe9e2761f00040a09779f416cbdf88772 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 19:23:44 -0400 Subject: [PATCH 076/451] message slicing --- tests/conftest.py | 28 ++++++++++++++++++++++++++++ tests/test_messages.py | 16 +++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2fe8fad2..a9d42c97 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -259,6 +259,34 @@ def mock_messages(api_url, account_id): ], "starred": False, "unread": True + }, { + "id": "1238", + "subject": "Test Message 2", + "account_id": account_id, + "object": "message", + "labels": [ + { + "name": "inbox", + "display_name": "Inbox", + "id": "abcd" + } + ], + "starred": False, + "unread": True + }, { + "id": "12", + "subject": "Test Message 3", + "account_id": account_id, + "object": "message", + "labels": [ + { + "name": "archive", + "display_name": "Archive", + "id": "gone" + } + ], + "starred": False, + "unread": False } ]) endpoint = re.compile(api_url + '/messages') diff --git a/tests/test_messages.py b/tests/test_messages.py index c501a2ee..2a498c10 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -3,6 +3,7 @@ import pytest import responses from urlobject import URLObject +from nylas.client.restful_models import Message @responses.activate @@ -26,6 +27,7 @@ def test_message_stars(api_client): message.unstar() assert message.starred is False + @responses.activate @pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") def test_message_read(api_client): @@ -58,12 +60,12 @@ def test_message_labels(api_client): @responses.activate -@pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") +@pytest.mark.usefixtures("mock_account", "mock_message", "mock_messages") def test_message_raw(api_client, account_id): message = api_client.messages.first() assert isinstance(message.raw, six.binary_type) parsed = json.loads(message.raw) - assert parsed == [{ + assert parsed == { "object": "message", "account_id": account_id, "labels": [{ @@ -75,7 +77,7 @@ def test_message_raw(api_client, account_id): "unread": True, "id": "1234", "subject": "Test Message" - }] + } @responses.activate @@ -86,3 +88,11 @@ def test_message_delete_by_id(api_client): request = responses.calls[0].request url = URLObject(request.url) assert url.query_dict["forceful"] == "True" + + +@responses.activate +@pytest.mark.usefixtures("mock_messages") +def test_slice_messages(api_client): + messages = api_client.messages[0:2] + assert len(messages) == 3 + assert all(isinstance(message, Message) for message in messages) From dec871034916e267d35674043766b0ac91f6d2a2 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 17 Jul 2017 19:34:32 -0400 Subject: [PATCH 077/451] Stringy 'None' for backwards compatibility --- nylas/client/client.py | 2 +- tests/test_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index be06d27f..e5748a45 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -145,7 +145,7 @@ def access_token(self, value): def authentication_url(self, redirect_uri, login_hint=''): args = {'redirect_uri': redirect_uri, - 'client_id': self.app_id or '', + 'client_id': self.app_id or 'None', # 'None' for back-compat 'response_type': 'code', 'scope': 'email', 'login_hint': login_hint, diff --git a/tests/test_client.py b/tests/test_client.py index 27db8ff7..aa14a52b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -60,7 +60,7 @@ def test_client_authentication_url(api_client, api_url): ('state', ''), ('redirect_uri', '/redirect'), ('response_type', 'code'), - ('client_id', ''), + ('client_id', 'None'), ('scope', 'email'), ]) ) From 59a6858ed90da921cbd36f080f4ab133f2dbc820 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jul 2017 16:55:57 -0400 Subject: [PATCH 078/451] Add hosted OAuth example This example uses Flask and Flask-Dance to demonstrate how to connect to Nylas via OAuth. --- examples-new/hosted-oauth/README.md | 85 ++++++++++++++++++ examples-new/hosted-oauth/config.json | 5 ++ examples-new/hosted-oauth/requirements.txt | 2 + examples-new/hosted-oauth/server.py | 88 +++++++++++++++++++ .../templates/after_authorized.html | 18 ++++ examples-new/hosted-oauth/templates/base.html | 16 ++++ .../templates/before_authorized.html | 24 +++++ 7 files changed, 238 insertions(+) create mode 100644 examples-new/hosted-oauth/README.md create mode 100644 examples-new/hosted-oauth/config.json create mode 100644 examples-new/hosted-oauth/requirements.txt create mode 100644 examples-new/hosted-oauth/server.py create mode 100644 examples-new/hosted-oauth/templates/after_authorized.html create mode 100644 examples-new/hosted-oauth/templates/base.html create mode 100644 examples-new/hosted-oauth/templates/before_authorized.html diff --git a/examples-new/hosted-oauth/README.md b/examples-new/hosted-oauth/README.md new file mode 100644 index 00000000..0480aa2e --- /dev/null +++ b/examples-new/hosted-oauth/README.md @@ -0,0 +1,85 @@ +# Example: Hosted OAuth + +This is an example project that demonstrates how to connect to Nylas via +OAuth. [OAuth](https://oauth.net/) is a standard protocol to allow two +websites to securely communicate with each other. + +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. +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 an API ID & API Secret from Nylas + +To do this, make a [Nylas Developer](https://developer.nylas.com/) account. +You should see your API ID and API 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 +API ID and API 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 Developer](https://developer.nylas.com) console, +click on the "Settings" button, and then select the "Callbacks" tab. +Paste your HTTPS URL into 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 "Callbacks" tab. + +Then click the "Done" 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 `http://127.0.0.1:5000/` in your browser +to test it out! diff --git a/examples-new/hosted-oauth/config.json b/examples-new/hosted-oauth/config.json new file mode 100644 index 00000000..8c100103 --- /dev/null +++ b/examples-new/hosted-oauth/config.json @@ -0,0 +1,5 @@ +{ + "SECRET_KEY": "replace me with a random string", + "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", + "NYLAS_OAUTH_API_SECRET": "replace me with the API Secret from Nylas" +} diff --git a/examples-new/hosted-oauth/requirements.txt b/examples-new/hosted-oauth/requirements.txt new file mode 100644 index 00000000..50a5e8f7 --- /dev/null +++ b/examples-new/hosted-oauth/requirements.txt @@ -0,0 +1,2 @@ +Flask>=0.11 +Flask-Dance>=0.11.0 diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py new file mode 100644 index 00000000..0f2b1789 --- /dev/null +++ b/examples-new/hosted-oauth/server.py @@ -0,0 +1,88 @@ +# 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: + from flask import Flask, request, render_template + 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") + +# 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. Check for insecure HTTP, so the template + # can display a handy warning if necessary. + oauth_ok = request.is_secure or os.environ.get("OAUTHLIB_INSECURE_TRANSPORT") + return render_template("before_authorized.html", oauth_ok=oauth_ok) + + # 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( + app_id=app.config["NYLAS_OAUTH_API_ID"], + app_secret=app.config["NYLAS_OAUTH_API_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) + +# When this file is executed, run the Flask web server. +if __name__ == "__main__": + app.run() diff --git a/examples-new/hosted-oauth/templates/after_authorized.html b/examples-new/hosted-oauth/templates/after_authorized.html new file mode 100644 index 00000000..0f4e7987 --- /dev/null +++ b/examples-new/hosted-oauth/templates/after_authorized.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block body %} +

Nylas Hosted OAuth Example

+

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() %} + + + + + {% endfor %} +
{{ key }}{{ value }}
+ +

If you want to test this OAuth flow again, clear your browser cookies + or open a new browser in incognito mode.

+{% endblock %} diff --git a/examples-new/hosted-oauth/templates/base.html b/examples-new/hosted-oauth/templates/base.html new file mode 100644 index 00000000..412c4b92 --- /dev/null +++ b/examples-new/hosted-oauth/templates/base.html @@ -0,0 +1,16 @@ + + + + + Nylas Hosted OAuth Example + + + + {% block body %}{% endblock %} + + diff --git a/examples-new/hosted-oauth/templates/before_authorized.html b/examples-new/hosted-oauth/templates/before_authorized.html new file mode 100644 index 00000000..fad17391 --- /dev/null +++ b/examples-new/hosted-oauth/templates/before_authorized.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% block body %} +

Nylas Hosted OAuth Example

+ +{% if not oauth_ok %} +
+

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 link + to connect to Nylas via hosted OAuth.

+
Connect to Nylas +{% endblock %} From 515a12902042ce5335bf66ed18587ef7bf51ddab Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jul 2017 17:10:10 -0400 Subject: [PATCH 079/451] Do HTTPS check in Javascript Since Flask doesn't know if it's behind ngrok or not --- examples-new/hosted-oauth/server.py | 10 ++++++---- examples-new/hosted-oauth/templates/base.html | 3 +++ .../hosted-oauth/templates/before_authorized.html | 11 +++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index 0f2b1789..65fb0c5b 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -65,10 +65,12 @@ 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. Check for insecure HTTP, so the template - # can display a handy warning if necessary. - oauth_ok = request.is_secure or os.environ.get("OAUTHLIB_INSECURE_TRANSPORT") - return render_template("before_authorized.html", oauth_ok=oauth_ok) + # 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: diff --git a/examples-new/hosted-oauth/templates/base.html b/examples-new/hosted-oauth/templates/base.html index 412c4b92..3e668139 100644 --- a/examples-new/hosted-oauth/templates/base.html +++ b/examples-new/hosted-oauth/templates/base.html @@ -8,6 +8,9 @@ border: 2px solid red; padding: 0 1em; } + .hide { + display: none; + } diff --git a/examples-new/hosted-oauth/templates/before_authorized.html b/examples-new/hosted-oauth/templates/before_authorized.html index fad17391..9a17e43b 100644 --- a/examples-new/hosted-oauth/templates/before_authorized.html +++ b/examples-new/hosted-oauth/templates/before_authorized.html @@ -2,8 +2,8 @@ {% block body %}

Nylas Hosted OAuth Example

-{% if not oauth_ok %} -
+{% 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. @@ -21,4 +21,11 @@

Warning

Thanks for giving Nylas a try! Next, you need to click this link to connect to Nylas via hosted OAuth.

Connect to Nylas + + {% endblock %} From 87943d0b4227a2ab82346da6b8188176b52ae678 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jul 2017 17:26:37 -0400 Subject: [PATCH 080/451] Direct people to use ngrok URL --- examples-new/hosted-oauth/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples-new/hosted-oauth/README.md b/examples-new/hosted-oauth/README.md index 0480aa2e..be9b384e 100644 --- a/examples-new/hosted-oauth/README.md +++ b/examples-new/hosted-oauth/README.md @@ -81,5 +81,4 @@ 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! +Once the server is running, visit the ngrok URL in your browser to test it out! From d2802bdbabf5c3923074440d03dd9c5cdcb5b7c6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jul 2017 17:45:24 -0400 Subject: [PATCH 081/451] Link to Nylas OAuth docs --- examples-new/hosted-oauth/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples-new/hosted-oauth/README.md b/examples-new/hosted-oauth/README.md index be9b384e..648a96de 100644 --- a/examples-new/hosted-oauth/README.md +++ b/examples-new/hosted-oauth/README.md @@ -1,12 +1,12 @@ # Example: Hosted OAuth This is an example project that demonstrates how to connect to Nylas via -OAuth. [OAuth](https://oauth.net/) is a standard protocol to allow two -websites to securely communicate with each other. +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. +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 From 84c59d2657d01e94f4f7e8f14e0aaf42e7402f7a Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 24 Jul 2017 17:58:50 -0400 Subject: [PATCH 082/451] Use ProxyFix to teach Flask about ngrok Otherwise, it doesn't realize that it's using secure HTTPS --- examples-new/hosted-oauth/server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index 65fb0c5b..7cfb88b9 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -7,6 +7,7 @@ # Imports from third-party modules that this project depends on try: from flask import Flask, request, render_template + from werkzeug.contrib.fixers import ProxyFix from flask_dance.contrib.nylas import make_nylas_blueprint, nylas except ImportError: message = textwrap.dedent(""" @@ -59,6 +60,9 @@ 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(): From 2a8b6739dde49e5f030ccefb15d72566af2c1432 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 26 Jul 2017 20:43:36 -0400 Subject: [PATCH 083/451] Fix failing test --- tests/conftest.py | 73 ++++++++++++-------------------------------- tests/test_drafts.py | 7 ++--- 2 files changed, 23 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a9d42c97..05ac31bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -528,7 +528,7 @@ def mock_drafts(api_url): @pytest.fixture def mock_draft_saved_response(api_url): - response_body = json.dumps({ + draft_json = { "bcc": [], "body": "Cheers mate!", "cc": [], @@ -554,66 +554,33 @@ def mock_draft_saved_response(api_url): ], "unread": False, "version": 0 - }) + } - responses.add( + def request_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) + return (200, {}, json.dumps(updated_draft_json)) + + responses.add_callback( responses.POST, api_url + '/drafts/', content_type='application/json', - status=200, - body=response_body, - match_querystring=True + callback=request_callback, ) - -@pytest.fixture -def mock_draft_updated_response(api_url): - body = { - "bcc": [], - "body": "", - "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": "Stay polish, stay hungary", - "thread_id": "clm33kapdxkposgltof845v9s", - "to": [ - { - "email": "helena@nylas.com", - "name": "Helena Handbasket" - } - ], - "unread": False, - "version": 0 - } - - responses.add( + responses.add_callback( responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', - status=200, - body=json.dumps(body), - match_querystring=True - ) - - body['subject'] = 'Update #2' - url = api_url + '/drafts/2h111aefv8pzwzfykrn7hercj?random_query=true¶m2=param' - responses.add( - responses.PUT, - url, - content_type='application/json', - status=200, - body=json.dumps(body), - match_querystring=True + callback=request_callback, ) @@ -655,7 +622,7 @@ def mock_draft_sent_response(api_url): } ], "unread": False, - "version": 0 + "version": 0, } values = [(400, {}, "Couldn't send email"), diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 7b945c0a..6cfe89fc 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -7,8 +7,7 @@ @responses.activate @pytest.mark.usefixtures( - "mock_draft_saved_response", "mock_draft_updated_response", - "mock_draft_sent_response" + "mock_draft_saved_response", "mock_draft_sent_response" ) def test_save_send_draft(api_client): draft = api_client.drafts.create() @@ -18,8 +17,8 @@ def test_save_send_draft(api_client): draft.save() draft.subject = "Stay polish, stay hungary" - draft.save(random_query='true', param2='param') - assert draft.subject == 'Update #2' + draft.save() + assert draft.subject == "Stay polish, stay hungary" msg = draft.send() assert msg['thread_id'] == 'clm33kapdxkposgltof845v9s' From 3384c665db20deaac70fd462db7c4051832a4dd2 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Wed, 26 Jul 2017 20:54:10 -0400 Subject: [PATCH 084/451] Fix test for Python 2 --- tests/test_send_error_handling.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_send_error_handling.py b/tests/test_send_error_handling.py index 1bd3d0f4..96111b69 100644 --- a/tests/test_send_error_handling.py +++ b/tests/test_send_error_handling.py @@ -2,6 +2,7 @@ import re import pytest import responses +import six from nylas.client.errors import ( MessageRejectedError, SendingQuotaExceededError, ServiceUnavailableError, ) @@ -14,6 +15,10 @@ def mock_sending_error(http_code, message, api_url, server_error=None): "message": message } + if six.PY2 and http_code == 429: + # Python 2 `httplib` doesn't know about status code 429 + six.moves.http_client.responses[429] = "Too Many Requests" + if server_error is not None: response_body['server_error'] = server_error From a855da5ef130c2f462ae0fcdc140455e8b163c81 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 12:22:12 -0400 Subject: [PATCH 085/451] Refactor to increase test coverage --- nylas/client/client.py | 56 +++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index e5748a45..8278ae5f 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -303,10 +303,11 @@ def _get_resource_data(self, cls, id, @nylas_excepted def _create_resource(self, cls, data, **kwargs): - url = "{}/{}/".format(self.api_server, cls.collection_name) - - if kwargs: - url = "{}?{}".format(url, urlencode(kwargs)) + url = ( + URLObject(self.api_server) + .with_path("/{name}/".format(name=cls.collection_name)) + .set_query_params(**kwargs) + ) session = self._get_http_session(cls.api_root) @@ -325,7 +326,10 @@ def _create_resource(self, cls, data, **kwargs): @nylas_excepted def _create_resources(self, cls, data): - url = "{}/{}/".format(self.api_server, cls.collection_name) + url = ( + URLObject(self.api_server) + .with_path("/{name}/".format(name=cls.collection_name)) + ) session = self._get_http_session(cls.api_root) if cls == File: @@ -341,11 +345,11 @@ def _create_resources(self, cls, data): @nylas_excepted def _delete_resource(self, cls, id, data=None, **kwargs): - name = cls.collection_name - url = "{}/{}/{}".format(self.api_server, name, id) - - if kwargs: - url = "{}?{}".format(url, urlencode(kwargs)) + url = ( + URLObject(self.api_server) + .with_path("/{name}/{id}".format(name=cls.collection_name, id=id)) + .set_query_params(**kwargs) + ) session = self._get_http_session(cls.api_root) if data: _validate(session.delete(url, json=data)) @@ -354,11 +358,11 @@ def _delete_resource(self, cls, id, data=None, **kwargs): @nylas_excepted def _update_resource(self, cls, id, data, **kwargs): - name = cls.collection_name - url = "{}/{}/{}".format(self.api_server, name, id) - - if kwargs: - url = "{}?{}".format(url, urlencode(kwargs)) + url = ( + URLObject(self.api_server) + .with_path("/{name}/{id}".format(name=cls.collection_name, id=id)) + .set_query_params(**kwargs) + ) session = self._get_http_session(cls.api_root) @@ -371,19 +375,25 @@ def _update_resource(self, cls, id, data, **kwargs): def _call_resource_method(self, cls, id, method_name, data): """POST a dictionary to an API method, for example /a/.../accounts/id/upgrade""" - name = cls.collection_name + if cls.api_root != 'a': - url = "{}/{}/{}/{}".format(self.api_server, name, id, method_name) + url_path = "/{name}/{id}/{method}".format( + name=cls.collection_name, id=id, method=method_name + ) else: # Management method. - url = "{}/a/{}/{}/{}/{}".format( - self.api_server, - self.app_id, - cls.collection_name, - id, - method_name, + url_path = "/a/{app_id}/{name}/{id}/{method}".format( + app_id=self.app_id, + name=cls.collection_name, + id=id, + method=method_name, ) + url = ( + URLObject(self.api_server) + .with_path(url_path) + ) + session = self._get_http_session(cls.api_root) response = session.post(url, json=data) From a721df21d15825358e3e3bc43a58b43207619a3e Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 12:32:14 -0400 Subject: [PATCH 086/451] Add a test to increase coverage --- tests/test_client.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 5b8ab13c..8a8590de 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -161,3 +161,25 @@ def test_create_resources(api_client, api_url): assert len(contacts) == 2 assert all(isinstance(contact, Contact) for contact in contacts) assert len(responses.calls) == 1 + + +@responses.activate +def test_call_resource_method(api_client, api_url): + contact_data = { + "id": 1, + "name": "first", + "email": "first@example.com", + } + responses.add( + responses.POST, + api_url + "/contacts/1/remove_duplicates", + content_type='application/json', + status=200, + body=json.dumps(contact_data), + ) + + contact = api_client._call_resource_method( + Contact, 1, "remove_duplicates", {} + ) + assert isinstance(contact, Contact) + assert len(responses.calls) == 1 From 701855b3fae0d060e5295853062d455cb76444ba Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 11:01:53 -0400 Subject: [PATCH 087/451] Added native authentication example --- examples-new/native-authentication/README.md | 107 ++++++++++ .../native-authentication/config.json | 7 + .../native-authentication/requirements.txt | 3 + examples-new/native-authentication/server.py | 183 ++++++++++++++++++ .../templates/after_connected.html | 17 ++ .../templates/after_google.html | 12 ++ .../native-authentication/templates/base.html | 19 ++ .../templates/before_google.html | 36 ++++ 8 files changed, 384 insertions(+) create mode 100644 examples-new/native-authentication/README.md create mode 100644 examples-new/native-authentication/config.json create mode 100644 examples-new/native-authentication/requirements.txt create mode 100644 examples-new/native-authentication/server.py create mode 100644 examples-new/native-authentication/templates/after_connected.html create mode 100644 examples-new/native-authentication/templates/after_google.html create mode 100644 examples-new/native-authentication/templates/base.html create mode 100644 examples-new/native-authentication/templates/before_google.html diff --git a/examples-new/native-authentication/README.md b/examples-new/native-authentication/README.md new file mode 100644 index 00000000..d4dfdb01 --- /dev/null +++ b/examples-new/native-authentication/README.md @@ -0,0 +1,107 @@ +# Example: Native Authentication + +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 an API ID & API Secret from Nylas + +To do this, make a [Nylas Developer](https://developer.nylas.com/) account. +You should see your API ID and API 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 API ID and API 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-new/native-authentication/config.json b/examples-new/native-authentication/config.json new file mode 100644 index 00000000..062e4afe --- /dev/null +++ b/examples-new/native-authentication/config.json @@ -0,0 +1,7 @@ +{ + "SECRET_KEY": "replace me with a random string", + "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", + "NYLAS_OAUTH_API_SECRET": "replace me with the API 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-new/native-authentication/requirements.txt b/examples-new/native-authentication/requirements.txt new file mode 100644 index 00000000..2d27e97d --- /dev/null +++ b/examples-new/native-authentication/requirements.txt @@ -0,0 +1,3 @@ +Flask>=0.11 +Flask-Dance>=0.11.0 +requests diff --git a/examples-new/native-authentication/server.py b/examples-new/native-authentication/server.py new file mode 100644 index 00000000..5f9bdafa --- /dev/null +++ b/examples-new/native-authentication/server.py @@ -0,0 +1,183 @@ +# 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.contrib.fixers 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=[ + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile", + "https://mail.google.com/", + "https://www.google.com/m8/feeds", + "https://www.googleapis.com/auth/calendar", + ], + offline=True, # this allows you to get a refresh token from Google + redirect_to="after_google", +) +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. Otherwise, it will be False. + if not google.authorized: + # 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("connect_to_nylas")) + + # 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( + app_id=app.config["NYLAS_OAUTH_API_ID"], + app_secret=app.config["NYLAS_OAUTH_API_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) + + +@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 + + # 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_API_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_API_ID"], + "client_secret": app.config["NYLAS_OAUTH_API_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")) + + +# When this file is executed, run the Flask web server. +if __name__ == "__main__": + app.run() diff --git a/examples-new/native-authentication/templates/after_connected.html b/examples-new/native-authentication/templates/after_connected.html new file mode 100644 index 00000000..a0affb29 --- /dev/null +++ b/examples-new/native-authentication/templates/after_connected.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block body %} +

Nylas Native Authentication Example

+

Done!

+

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() %} + + + + + {% endfor %} +
{{ key }}{{ value }}
+ +{% endblock %} diff --git a/examples-new/native-authentication/templates/after_google.html b/examples-new/native-authentication/templates/after_google.html new file mode 100644 index 00000000..b7d7e1d6 --- /dev/null +++ b/examples-new/native-authentication/templates/after_google.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block body %} +

Nylas Native Authentication Example

+

Step 2: Pass Credentials to Nylas

+ +

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.

+Pass Credentials to Nylas + +{% endblock %} diff --git a/examples-new/native-authentication/templates/base.html b/examples-new/native-authentication/templates/base.html new file mode 100644 index 00000000..86dc9abb --- /dev/null +++ b/examples-new/native-authentication/templates/base.html @@ -0,0 +1,19 @@ + + + + + Nylas Native Authentication Example + + + + {% block body %}{% endblock %} + + diff --git a/examples-new/native-authentication/templates/before_google.html b/examples-new/native-authentication/templates/before_google.html new file mode 100644 index 00000000..c02e888c --- /dev/null +++ b/examples-new/native-authentication/templates/before_google.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block body %} +

Nylas Native Authentication Example

+ +{% 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 link to connect with Google.

+Connect with Google + + +{% endblock %} From 032eb7dd02505ddef4b3f91fe03836e41500ff0e Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 11:02:03 -0400 Subject: [PATCH 088/451] Remove unused import --- examples-new/hosted-oauth/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index 7cfb88b9..b0553396 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -6,7 +6,7 @@ # Imports from third-party modules that this project depends on try: - from flask import Flask, request, render_template + from flask import Flask, render_template from werkzeug.contrib.fixers import ProxyFix from flask_dance.contrib.nylas import make_nylas_blueprint, nylas except ImportError: From f0e4ea09c6487474f6e278fcd4467496c152736d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 11:03:18 -0400 Subject: [PATCH 089/451] Remove extra space --- examples-new/hosted-oauth/server.py | 2 +- examples-new/native-authentication/server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index b0553396..f42dc237 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -41,7 +41,7 @@ # 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() + key for key, value in app.config.items() if isinstance(value, str) and value.startswith("replace me") ] if cfg_needs_replacing: diff --git a/examples-new/native-authentication/server.py b/examples-new/native-authentication/server.py index 5f9bdafa..f638afab 100644 --- a/examples-new/native-authentication/server.py +++ b/examples-new/native-authentication/server.py @@ -42,7 +42,7 @@ # 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() + key for key, value in app.config.items() if isinstance(value, str) and value.startswith("replace me") ] if cfg_needs_replacing: From 46b68b7a0cb171d45c70020825fa4e293a9d13a3 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 11:19:24 -0400 Subject: [PATCH 090/451] Oops, fix redirect --- examples-new/native-authentication/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-new/native-authentication/server.py b/examples-new/native-authentication/server.py index f638afab..e8cd7633 100644 --- a/examples-new/native-authentication/server.py +++ b/examples-new/native-authentication/server.py @@ -91,7 +91,7 @@ def index(): # 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("connect_to_nylas")) + return redirect(url_for("pass_creds_to_nylas")) # If we've gotten to this point, then the user has already connected # to both Google and Nylas. From 2f39449ee75783fec9b64827a213afd88200edef Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 18:08:28 -0400 Subject: [PATCH 091/451] Display ngrok URL on server start --- examples-new/hosted-oauth/requirements.txt | 1 + examples-new/hosted-oauth/server.py | 24 ++++++++++++++++++++ examples-new/native-authentication/server.py | 22 ++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/examples-new/hosted-oauth/requirements.txt b/examples-new/hosted-oauth/requirements.txt index 50a5e8f7..2d27e97d 100644 --- a/examples-new/hosted-oauth/requirements.txt +++ b/examples-new/hosted-oauth/requirements.txt @@ -1,2 +1,3 @@ Flask>=0.11 Flask-Dance>=0.11.0 +requests diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index f42dc237..715a3cd5 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -6,6 +6,7 @@ # Imports from third-party modules that this project depends on try: + import requests from flask import Flask, render_template from werkzeug.contrib.fixers import ProxyFix from flask_dance.contrib.nylas import make_nylas_blueprint, nylas @@ -89,6 +90,29 @@ def index(): 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 + """ + ngrok_resp = requests.get("http://localhost:4040/api/tunnels") + if not ngrok_resp.ok: + # 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-new/native-authentication/server.py b/examples-new/native-authentication/server.py index e8cd7633..ecde63d1 100644 --- a/examples-new/native-authentication/server.py +++ b/examples-new/native-authentication/server.py @@ -178,6 +178,28 @@ def pass_creds_to_nylas(): 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 + """ + ngrok_resp = requests.get("http://localhost:4040/api/tunnels") + if not ngrok_resp.ok: + # 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() From f9773609c5fd222ccb4ea835f2d47b1eabcd66b1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 25 Jul 2017 18:33:29 -0400 Subject: [PATCH 092/451] Add guidance for how to fix missing Google refresh token --- examples-new/native-authentication/server.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/examples-new/native-authentication/server.py b/examples-new/native-authentication/server.py index ecde63d1..7f0aa689 100644 --- a/examples-new/native-authentication/server.py +++ b/examples-new/native-authentication/server.py @@ -68,6 +68,13 @@ ], 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") @@ -91,7 +98,7 @@ def index(): # 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("pass_creds_to_nylas")) + return redirect(url_for("after_google")) # If we've gotten to this point, then the user has already connected # to both Google and Nylas. @@ -128,6 +135,14 @@ def pass_creds_to_nylas(): 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" From c316d2ebbe4569840e3d009ec4e54239aaccd3b1 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 15:33:23 -0400 Subject: [PATCH 093/451] Bootstrap for native authentication gmail --- .../README.md | 0 .../config.json | 0 .../requirements.txt | 0 .../server.py | 11 +++++++---- .../templates/after_connected.html | 3 +-- .../templates/after_google.html | 3 +-- .../templates/base.html | 14 ++++++++++++++ .../templates/before_google.html | 12 +++++------- .../native-authentication/templates/base.html | 19 ------------------- 9 files changed, 28 insertions(+), 34 deletions(-) rename examples-new/{native-authentication => native-authentication-gmail}/README.md (100%) rename examples-new/{native-authentication => native-authentication-gmail}/config.json (100%) rename examples-new/{native-authentication => native-authentication-gmail}/requirements.txt (100%) rename examples-new/{native-authentication => native-authentication-gmail}/server.py (95%) rename examples-new/{native-authentication => native-authentication-gmail}/templates/after_connected.html (86%) rename examples-new/{native-authentication => native-authentication-gmail}/templates/after_google.html (73%) create mode 100644 examples-new/native-authentication-gmail/templates/base.html rename examples-new/{native-authentication => native-authentication-gmail}/templates/before_google.html (77%) delete mode 100644 examples-new/native-authentication/templates/base.html diff --git a/examples-new/native-authentication/README.md b/examples-new/native-authentication-gmail/README.md similarity index 100% rename from examples-new/native-authentication/README.md rename to examples-new/native-authentication-gmail/README.md diff --git a/examples-new/native-authentication/config.json b/examples-new/native-authentication-gmail/config.json similarity index 100% rename from examples-new/native-authentication/config.json rename to examples-new/native-authentication-gmail/config.json diff --git a/examples-new/native-authentication/requirements.txt b/examples-new/native-authentication-gmail/requirements.txt similarity index 100% rename from examples-new/native-authentication/requirements.txt rename to examples-new/native-authentication-gmail/requirements.txt diff --git a/examples-new/native-authentication/server.py b/examples-new/native-authentication-gmail/server.py similarity index 95% rename from examples-new/native-authentication/server.py rename to examples-new/native-authentication-gmail/server.py index 7f0aa689..0b5f6db3 100644 --- a/examples-new/native-authentication/server.py +++ b/examples-new/native-authentication-gmail/server.py @@ -85,8 +85,10 @@ @app.route("/") def index(): # If the user has already connected to Google via OAuth, - # `google.authorized` will be True. Otherwise, it will be False. - if not google.authorized: + # `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( @@ -199,8 +201,9 @@ def ngrok_url(): to figure out what URL it has assigned, and suggest that to the user. https://ngrok.com/docs#list-tunnels """ - ngrok_resp = requests.get("http://localhost:4040/api/tunnels") - if not ngrok_resp.ok: + 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() diff --git a/examples-new/native-authentication/templates/after_connected.html b/examples-new/native-authentication-gmail/templates/after_connected.html similarity index 86% rename from examples-new/native-authentication/templates/after_connected.html rename to examples-new/native-authentication-gmail/templates/after_connected.html index a0affb29..b9a78636 100644 --- a/examples-new/native-authentication/templates/after_connected.html +++ b/examples-new/native-authentication-gmail/templates/after_connected.html @@ -1,11 +1,10 @@ {% extends "base.html" %} {% block body %} -

Nylas Native Authentication Example

Done!

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() %} diff --git a/examples-new/native-authentication/templates/after_google.html b/examples-new/native-authentication-gmail/templates/after_google.html similarity index 73% rename from examples-new/native-authentication/templates/after_google.html rename to examples-new/native-authentication-gmail/templates/after_google.html index b7d7e1d6..89a8fc09 100644 --- a/examples-new/native-authentication/templates/after_google.html +++ b/examples-new/native-authentication-gmail/templates/after_google.html @@ -1,12 +1,11 @@ {% extends "base.html" %} {% block body %} -

Nylas Native Authentication Example

Step 2: Pass Credentials to Nylas

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.

-Pass Credentials to Nylas +Pass Credentials to Nylas {% endblock %} diff --git a/examples-new/native-authentication-gmail/templates/base.html b/examples-new/native-authentication-gmail/templates/base.html new file mode 100644 index 00000000..9a26d102 --- /dev/null +++ b/examples-new/native-authentication-gmail/templates/base.html @@ -0,0 +1,14 @@ + + + + + Nylas Native Authentication: Gmail + + + +
+

Nylas Native Authentication: Gmail

+ {% block body %}{% endblock %} +
+ + diff --git a/examples-new/native-authentication/templates/before_google.html b/examples-new/native-authentication-gmail/templates/before_google.html similarity index 77% rename from examples-new/native-authentication/templates/before_google.html rename to examples-new/native-authentication-gmail/templates/before_google.html index c02e888c..07a59634 100644 --- a/examples-new/native-authentication/templates/before_google.html +++ b/examples-new/native-authentication-gmail/templates/before_google.html @@ -1,9 +1,7 @@ {% extends "base.html" %} {% block body %} -

Nylas Native Authentication Example

- {% if not insecure_override %} -
+
{{ key }}
+
{% for key, value in account.items() %} diff --git a/examples-new/hosted-oauth/templates/base.html b/examples-new/hosted-oauth/templates/base.html index 3e668139..e3f8e610 100644 --- a/examples-new/hosted-oauth/templates/base.html +++ b/examples-new/hosted-oauth/templates/base.html @@ -3,17 +3,12 @@ Nylas Hosted OAuth Example - + - {% block body %}{% endblock %} +
+

Nylas Hosted OAuth Example

+ {% block body %}{% endblock %} +
diff --git a/examples-new/hosted-oauth/templates/before_authorized.html b/examples-new/hosted-oauth/templates/before_authorized.html index 9a17e43b..4488e7a1 100644 --- a/examples-new/hosted-oauth/templates/before_authorized.html +++ b/examples-new/hosted-oauth/templates/before_authorized.html @@ -3,7 +3,7 @@

Nylas Hosted OAuth Example

{% if not insecure_override %} -
+ {% endif %} -

Thanks for giving Nylas a try! Next, you need to click this link +

Thanks for giving Nylas a try! Next, you need to click this button to connect to Nylas via hosted OAuth.

-Connect to Nylas +Connect to Nylas {% endblock %} From 749ad2884b3b39bfe154f03ad2178ddec9900164 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 15:48:32 -0400 Subject: [PATCH 095/451] Fix Travis badge in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9bdabf72..1f3b97b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nylas REST API Python bindings ![Travis build status](https://travis-ci.org/nylas/nylas-python.svg?branch=master) [![Code Coverage](https://codecov.io/gh/nylas/nylas-python/branch/master/graph/badge.svg)](https://codecov.io/gh/nylas/nylas-python) +# Nylas REST API Python bindings [![Build Status](https://travis-ci.org/nylas/nylas-python.svg?branch=master)](https://travis-ci.org/nylas/nylas-python) [![Code Coverage](https://codecov.io/gh/nylas/nylas-python/branch/master/graph/badge.svg)](https://codecov.io/gh/nylas/nylas-python) Python bindings for the Nylas REST API. https://www.nylas.com/docs From 8606c777b16ab6bd7246c75a83c8aca1120cb14d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 15:56:01 -0400 Subject: [PATCH 096/451] Specify what kind of native authentication this is --- examples-new/native-authentication-gmail/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-new/native-authentication-gmail/README.md b/examples-new/native-authentication-gmail/README.md index d4dfdb01..5990dde9 100644 --- a/examples-new/native-authentication-gmail/README.md +++ b/examples-new/native-authentication-gmail/README.md @@ -1,4 +1,4 @@ -# Example: Native Authentication +# 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) From 13034a313bcf2d7b854d1a132dc0242e8d559db9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 16:01:05 -0400 Subject: [PATCH 097/451] Fix ngrok failure --- examples-new/hosted-oauth/server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index 715a3cd5..09d11d3d 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -97,8 +97,9 @@ def ngrok_url(): to figure out what URL it has assigned, and suggest that to the user. https://ngrok.com/docs#list-tunnels """ - ngrok_resp = requests.get("http://localhost:4040/api/tunnels") - if not ngrok_resp.ok: + 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() From c3b75efa639fbcde90d9475325d6d29ad8ed58a0 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 27 Jul 2017 15:33:44 -0400 Subject: [PATCH 098/451] Add native auth example for Exchange --- .../native-authentication-exchange/README.md | 51 ++++++ .../config.json | 5 + .../requirements.txt | 3 + .../native-authentication-exchange/server.py | 166 ++++++++++++++++++ .../templates/base.html | 14 ++ .../templates/index.html | 27 +++ .../templates/missing_token.html | 5 + .../templates/success.html | 16 ++ 8 files changed, 287 insertions(+) create mode 100644 examples-new/native-authentication-exchange/README.md create mode 100644 examples-new/native-authentication-exchange/config.json create mode 100644 examples-new/native-authentication-exchange/requirements.txt create mode 100644 examples-new/native-authentication-exchange/server.py create mode 100644 examples-new/native-authentication-exchange/templates/base.html create mode 100644 examples-new/native-authentication-exchange/templates/index.html create mode 100644 examples-new/native-authentication-exchange/templates/missing_token.html create mode 100644 examples-new/native-authentication-exchange/templates/success.html diff --git a/examples-new/native-authentication-exchange/README.md b/examples-new/native-authentication-exchange/README.md new file mode 100644 index 00000000..d341d9b2 --- /dev/null +++ b/examples-new/native-authentication-exchange/README.md @@ -0,0 +1,51 @@ +# 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 an API ID & API Secret from Nylas + +To do this, make a [Nylas Developer](https://developer.nylas.com/) account. +You should see your API ID and API 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 API ID and +API 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-new/native-authentication-exchange/config.json b/examples-new/native-authentication-exchange/config.json new file mode 100644 index 00000000..8c100103 --- /dev/null +++ b/examples-new/native-authentication-exchange/config.json @@ -0,0 +1,5 @@ +{ + "SECRET_KEY": "replace me with a random string", + "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", + "NYLAS_OAUTH_API_SECRET": "replace me with the API Secret from Nylas" +} diff --git a/examples-new/native-authentication-exchange/requirements.txt b/examples-new/native-authentication-exchange/requirements.txt new file mode 100644 index 00000000..f03e8299 --- /dev/null +++ b/examples-new/native-authentication-exchange/requirements.txt @@ -0,0 +1,3 @@ +Flask>=0.11 +Flask-WTF +requests diff --git a/examples-new/native-authentication-exchange/server.py b/examples-new/native-authentication-exchange/server.py new file mode 100644 index 00000000..52f9ca5b --- /dev/null +++ b/examples-new/native-authentication-exchange/server.py @@ -0,0 +1,166 @@ +# 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', validators=[DataRequired()]) + + +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): + """ + 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_API_ID"], + "name": name, + "email_address": email, + "provider": "exchange", + "settings": { + "username": email, + "password": password, + "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_API_ID"], + "client_secret": app.config["NYLAS_OAUTH_API_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_account_token" not in session: + return render_template("missing_token.html") + + client = APIClient( + app_id=app.config["NYLAS_OAUTH_API_ID"], + app_secret=app.config["NYLAS_OAUTH_API_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-new/native-authentication-exchange/templates/base.html b/examples-new/native-authentication-exchange/templates/base.html new file mode 100644 index 00000000..b0683cbb --- /dev/null +++ b/examples-new/native-authentication-exchange/templates/base.html @@ -0,0 +1,14 @@ + + + + + Nylas Native Authentication: Exchange + + + +
+

Nylas Native Authentication: Exchange

+ {% block body %}{% endblock %} +
+ + diff --git a/examples-new/native-authentication-exchange/templates/index.html b/examples-new/native-authentication-exchange/templates/index.html new file mode 100644 index 00000000..20a653b2 --- /dev/null +++ b/examples-new/native-authentication-exchange/templates/index.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block body %} +

Thanks for giving Nylas a try! + This example is set up to work with Exchange, so it will only work if you + have an Exchange account.

+ +
+

Please enter the credentials for your Microsoft Exchange account.

+ {{ form.hidden_tag() }} + + {% if api_error %} +
{{ api_error }}
+ {% endif %} + + {% for field in form if field.widget.input_type != 'hidden' %} +
+ {{ field.label }} + {{ field(class_='form-control') }} + {% if field.errors %} + {% for error in field.errors %}{{ error }}{% if not loop.last %}
{% endif %}{% endfor %}
+ {% endif %} +
+ {% endfor %} + + + +{% endblock %} diff --git a/examples-new/native-authentication-exchange/templates/missing_token.html b/examples-new/native-authentication-exchange/templates/missing_token.html new file mode 100644 index 00000000..7b194ca5 --- /dev/null +++ b/examples-new/native-authentication-exchange/templates/missing_token.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block body %} +

Missing Access Token

+

You don't seem to have an OAuth access token for Nylas. Try going back to the home page to make one.

+{% endblock %} diff --git a/examples-new/native-authentication-exchange/templates/success.html b/examples-new/native-authentication-exchange/templates/success.html new file mode 100644 index 00000000..b9a78636 --- /dev/null +++ b/examples-new/native-authentication-exchange/templates/success.html @@ -0,0 +1,16 @@ +{% 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:

+ +
{{ key }}
+ {% for key, value in account.items() %} + + + + + {% endfor %} +
{{ key }}{{ value }}
+ +{% endblock %} From 86ab1c8942101661505995dc2d52265cfd103787 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 31 Jul 2017 14:13:07 -0400 Subject: [PATCH 099/451] server host is optional --- .../native-authentication-exchange/server.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples-new/native-authentication-exchange/server.py b/examples-new/native-authentication-exchange/server.py index 52f9ca5b..9d729cad 100644 --- a/examples-new/native-authentication-exchange/server.py +++ b/examples-new/native-authentication-exchange/server.py @@ -63,7 +63,9 @@ class ExchangeCredentialsForm(FlaskForm): name = StringField('Name', validators=[DataRequired()]) email = EmailField('Email Address', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) - server_host = StringField('Server Host', validators=[DataRequired()]) + server_host = StringField( + 'Server Host', render_kw={"placeholder": "(optional)"} + ) class APIError(Exception): @@ -88,7 +90,7 @@ def index(): return render_template("index.html", form=form, api_error=api_error) -def pass_creds_to_nylas(name, email, password, server_host): +def pass_creds_to_nylas(name, email, password, server_host=None): """ Passes Exchange credentials to Nylas, to set up native authentication. """ @@ -102,9 +104,11 @@ def pass_creds_to_nylas(name, email, password, server_host): "settings": { "username": email, "password": password, - "eas_server_host": server_host, } } + 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, @@ -146,7 +150,7 @@ def pass_creds_to_nylas(name, email, password, server_host): @app.route("/success") def success(): - if "nylas_account_token" not in session: + if "nylas_access_token" not in session: return render_template("missing_token.html") client = APIClient( From e48032916c84d5d091d139554ddda15c10e4fd9e Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 31 Jul 2017 20:10:08 -0400 Subject: [PATCH 100/451] "API ID" -> "client ID", "API Secret" -> "client secret" --- examples-new/hosted-oauth/README.md | 9 +++++---- examples-new/hosted-oauth/config.json | 4 ++-- examples-new/hosted-oauth/requirements.txt | 2 +- examples-new/hosted-oauth/server.py | 4 ++-- .../native-authentication-exchange/README.md | 11 ++++++----- .../native-authentication-exchange/config.json | 4 ++-- .../native-authentication-exchange/server.py | 10 +++++----- examples-new/native-authentication-gmail/README.md | 13 +++++++------ .../native-authentication-gmail/config.json | 4 ++-- .../native-authentication-gmail/requirements.txt | 2 +- examples-new/native-authentication-gmail/server.py | 10 +++++----- 11 files changed, 38 insertions(+), 35 deletions(-) diff --git a/examples-new/hosted-oauth/README.md b/examples-new/hosted-oauth/README.md index 648a96de..8ade81b2 100644 --- a/examples-new/hosted-oauth/README.md +++ b/examples-new/hosted-oauth/README.md @@ -14,16 +14,17 @@ 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 an API ID & API Secret from Nylas +## Get a client ID & client secret from Nylas To do this, make a [Nylas Developer](https://developer.nylas.com/) account. -You should see your API ID and API Secret on the dashboard, once you've logged -in on the [Nylas Developer](https://developer.nylas.com/) website. +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 -API ID and API Secret with the real values that you got from the Nylas +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. diff --git a/examples-new/hosted-oauth/config.json b/examples-new/hosted-oauth/config.json index 8c100103..9b54e3df 100644 --- a/examples-new/hosted-oauth/config.json +++ b/examples-new/hosted-oauth/config.json @@ -1,5 +1,5 @@ { "SECRET_KEY": "replace me with a random string", - "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", - "NYLAS_OAUTH_API_SECRET": "replace me with the API Secret from Nylas" + "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-new/hosted-oauth/requirements.txt b/examples-new/hosted-oauth/requirements.txt index 2d27e97d..e87c213b 100644 --- a/examples-new/hosted-oauth/requirements.txt +++ b/examples-new/hosted-oauth/requirements.txt @@ -1,3 +1,3 @@ Flask>=0.11 -Flask-Dance>=0.11.0 +Flask-Dance>=0.11.1 requests diff --git a/examples-new/hosted-oauth/server.py b/examples-new/hosted-oauth/server.py index 715a3cd5..c4205dfd 100644 --- a/examples-new/hosted-oauth/server.py +++ b/examples-new/hosted-oauth/server.py @@ -80,8 +80,8 @@ def index(): # 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( - app_id=app.config["NYLAS_OAUTH_API_ID"], - app_secret=app.config["NYLAS_OAUTH_API_SECRET"], + app_id=app.config["NYLAS_OAUTH_CLIENT_ID"], + app_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"], access_token=nylas.access_token, ) diff --git a/examples-new/native-authentication-exchange/README.md b/examples-new/native-authentication-exchange/README.md index d341d9b2..09ef1d0d 100644 --- a/examples-new/native-authentication-exchange/README.md +++ b/examples-new/native-authentication-exchange/README.md @@ -16,17 +16,18 @@ 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 an API ID & API Secret from Nylas +## Get a client ID & client secret from Nylas To do this, make a [Nylas Developer](https://developer.nylas.com/) account. -You should see your API ID and API Secret on the dashboard, once you've logged -in on the [Nylas Developer](https://developer.nylas.com/) website. +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 API ID and -API Secret fron Nylas. You'll also need to replace the example secret key with +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 diff --git a/examples-new/native-authentication-exchange/config.json b/examples-new/native-authentication-exchange/config.json index 8c100103..9b54e3df 100644 --- a/examples-new/native-authentication-exchange/config.json +++ b/examples-new/native-authentication-exchange/config.json @@ -1,5 +1,5 @@ { "SECRET_KEY": "replace me with a random string", - "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", - "NYLAS_OAUTH_API_SECRET": "replace me with the API Secret from Nylas" + "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-new/native-authentication-exchange/server.py b/examples-new/native-authentication-exchange/server.py index 9d729cad..5594c1fb 100644 --- a/examples-new/native-authentication-exchange/server.py +++ b/examples-new/native-authentication-exchange/server.py @@ -97,7 +97,7 @@ def pass_creds_to_nylas(name, email, password, server_host=None): # 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_API_ID"], + "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"], "name": name, "email_address": email, "provider": "exchange", @@ -122,8 +122,8 @@ def pass_creds_to_nylas(name, email, password, server_host=None): # 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_API_ID"], - "client_secret": app.config["NYLAS_OAUTH_API_SECRET"], + "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"], + "client_secret": app.config["NYLAS_OAUTH_CLIENT_SECRET"], "code": nylas_code, } nylas_token_resp = requests.post( @@ -154,8 +154,8 @@ def success(): return render_template("missing_token.html") client = APIClient( - app_id=app.config["NYLAS_OAUTH_API_ID"], - app_secret=app.config["NYLAS_OAUTH_API_SECRET"], + app_id=app.config["NYLAS_OAUTH_CLIENT_ID"], + app_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"], access_token=session["nylas_access_token"], ) diff --git a/examples-new/native-authentication-gmail/README.md b/examples-new/native-authentication-gmail/README.md index d4dfdb01..23707296 100644 --- a/examples-new/native-authentication-gmail/README.md +++ b/examples-new/native-authentication-gmail/README.md @@ -16,13 +16,14 @@ 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 an API ID & API Secret from Nylas +## Get a client ID & client secret from Nylas To do this, make a [Nylas Developer](https://developer.nylas.com/) account. -You should see your API ID and API Secret on the dashboard, once you've logged -in on the [Nylas Developer](https://developer.nylas.com/) website. +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 +## Get a client ID & client secret from Google To do this, go to the [Google Developers Console](https://console.developers.google.com) @@ -39,8 +40,8 @@ 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 API ID and API Secret from Nylas, -and the client ID and client secret from Google. +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. diff --git a/examples-new/native-authentication-gmail/config.json b/examples-new/native-authentication-gmail/config.json index 062e4afe..f997e0fe 100644 --- a/examples-new/native-authentication-gmail/config.json +++ b/examples-new/native-authentication-gmail/config.json @@ -1,7 +1,7 @@ { "SECRET_KEY": "replace me with a random string", - "NYLAS_OAUTH_API_ID": "replace me with the API ID from Nylas", - "NYLAS_OAUTH_API_SECRET": "replace me with the API Secret from Nylas", + "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-new/native-authentication-gmail/requirements.txt b/examples-new/native-authentication-gmail/requirements.txt index 2d27e97d..e87c213b 100644 --- a/examples-new/native-authentication-gmail/requirements.txt +++ b/examples-new/native-authentication-gmail/requirements.txt @@ -1,3 +1,3 @@ Flask>=0.11 -Flask-Dance>=0.11.0 +Flask-Dance>=0.11.1 requests diff --git a/examples-new/native-authentication-gmail/server.py b/examples-new/native-authentication-gmail/server.py index 0b5f6db3..73a6fd6c 100644 --- a/examples-new/native-authentication-gmail/server.py +++ b/examples-new/native-authentication-gmail/server.py @@ -106,8 +106,8 @@ def index(): # to both Google and Nylas. # Let's set up the SDK client with the OAuth token: client = APIClient( - app_id=app.config["NYLAS_OAUTH_API_ID"], - app_secret=app.config["NYLAS_OAUTH_API_SECRET"], + app_id=app.config["NYLAS_OAUTH_CLIENT_ID"], + app_secret=app.config["NYLAS_OAUTH_CLIENT_SECRET"], access_token=session["nylas_access_token"], ) @@ -153,7 +153,7 @@ def pass_creds_to_nylas(): # 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_API_ID"], + "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"], "name": google_userinfo["name"], "email_address": google_userinfo["email"], "provider": "gmail", @@ -173,8 +173,8 @@ def pass_creds_to_nylas(): # 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_API_ID"], - "client_secret": app.config["NYLAS_OAUTH_API_SECRET"], + "client_id": app.config["NYLAS_OAUTH_CLIENT_ID"], + "client_secret": app.config["NYLAS_OAUTH_CLIENT_SECRET"], "code": nylas_code, } nylas_token_resp = requests.post( From 3960e98bac28e587bc87f2e613e7bd17a58d1d63 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 14:56:05 -0400 Subject: [PATCH 101/451] Allow passing a `state` parameter to the authentication_url() method --- nylas/client/client.py | 4 ++-- tests/test_client.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 8278ae5f..b41f782e 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -143,13 +143,13 @@ def access_token(self, value): if 'Authorization' in self.session.headers: del self.session.headers['Authorization'] - def authentication_url(self, redirect_uri, login_hint=''): + def authentication_url(self, redirect_uri, login_hint='', state=''): args = {'redirect_uri': redirect_uri, 'client_id': self.app_id or 'None', # 'None' for back-compat 'response_type': 'code', 'scope': 'email', 'login_hint': login_hint, - 'state': ''} + 'state': state} url = URLObject(self.authorize_url).add_query_params(args.items()) return str(url) diff --git a/tests/test_client.py b/tests/test_client.py index 8a8590de..ea4831d3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -83,6 +83,12 @@ def test_client_authentication_url(api_client, api_url): expected2 = expected.set_query_param("login_hint", "hint") assert urls_equal(expected2, actual2) + actual3 = URLObject( + api_client.authentication_url("/redirect", state="confusion") + ) + expected3 = expected.set_query_param("state", "confusion") + assert urls_equal(expected3, actual3) + @responses.activate def test_client_token_for_code(api_client, api_url): From e25d799eef49cacdb9f0546029d09b4e5057a754 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 15:10:40 -0400 Subject: [PATCH 102/451] Fix xfail tests By correcting how child_collection() is called --- nylas/client/restful_models.py | 8 ++++---- tests/conftest.py | 10 +++++----- tests/test_folders.py | 2 -- tests/test_labels.py | 20 +++++++++++++++++++- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/nylas/client/restful_models.py b/nylas/client/restful_models.py index 93488a0d..e73c100e 100644 --- a/nylas/client/restful_models.py +++ b/nylas/client/restful_models.py @@ -179,11 +179,11 @@ def __init__(self, api): @property def threads(self): - return self.child_collection({'in': self.id}) + return self.child_collection(Thread, folder_id=self.id) @property def messages(self): - return self.child_collection({'in': self.id}) + return self.child_collection(Message, folder_id=self.id) class Label(NylasAPIObject): @@ -195,11 +195,11 @@ def __init__(self, api): @property def threads(self): - return self.child_collection({'in': self.id}) + return self.child_collection(Thread, label_id=self.id) @property def messages(self): - return self.child_collection({'in': self.id}) + return self.child_collection(Message, label_id=self.id) class Thread(NylasAPIObject): diff --git a/tests/conftest.py b/tests/conftest.py index 05ac31bf..a33ee024 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -199,10 +199,10 @@ def mock_label(api_url, account_id): "object": "label" } ) - endpoint = re.compile(api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') + url = api_url + '/labels/anuep8pe5ugmxrucchrzba2o8' responses.add( responses.GET, - endpoint, + url, content_type='application/json', status=200, body=response_body, @@ -219,10 +219,10 @@ def mock_folder(api_url, account_id): "object": "folder" } response_body = json.dumps(folder) - endpoint = re.compile(api_url + '/folders/anuep8pe5ug3xrupchwzba2o8') + url = api_url + '/folders/anuep8pe5ug3xrupchwzba2o8' responses.add( responses.GET, - endpoint, + url, content_type='application/json', status=200, body=response_body, @@ -236,7 +236,7 @@ def request_callback(request): responses.add_callback( responses.PUT, - endpoint, + url, content_type='application/json', callback=request_callback, ) diff --git a/tests/test_folders.py b/tests/test_folders.py index 50daf922..407c0d30 100644 --- a/tests/test_folders.py +++ b/tests/test_folders.py @@ -16,7 +16,6 @@ def test_get_change_folder(api_client): @responses.activate -@pytest.mark.xfail @pytest.mark.usefixtures("mock_folder", "mock_threads") def test_folder_threads(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') @@ -26,7 +25,6 @@ def test_folder_threads(api_client): @responses.activate -@pytest.mark.xfail @pytest.mark.usefixtures("mock_folder", "mock_messages") def test_folder_messages(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') diff --git a/tests/test_labels.py b/tests/test_labels.py index 48acd86a..74cf02e9 100644 --- a/tests/test_labels.py +++ b/tests/test_labels.py @@ -1,6 +1,6 @@ import pytest import responses -from nylas.client.restful_models import Label +from nylas.client.restful_models import Label, Thread, Message @responses.activate @@ -19,3 +19,21 @@ def test_get_label(api_client): assert label is not None assert isinstance(label, Label) assert label.display_name == 'Important' + + +@responses.activate +@pytest.mark.usefixtures("mock_label", "mock_threads") +def test_label_threads(api_client): + label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') + assert label.threads + assert all(isinstance(thread, Thread) + for thread in label.threads) + + +@responses.activate +@pytest.mark.usefixtures("mock_label", "mock_messages") +def test_label_messages(api_client): + label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') + assert label.messages + assert all(isinstance(message, Message) + for message in label.messages) From 055fddb1f05135c139859c6ace25cfcebcab9475 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 17:24:43 -0400 Subject: [PATCH 103/451] Added webhook example --- examples-new/webhooks/README.md | 79 +++++++++++ examples-new/webhooks/config.json | 5 + examples-new/webhooks/requirements.txt | 2 + examples-new/webhooks/server.py | 157 +++++++++++++++++++++ examples-new/webhooks/templates/base.html | 14 ++ examples-new/webhooks/templates/index.html | 12 ++ 6 files changed, 269 insertions(+) create mode 100644 examples-new/webhooks/README.md create mode 100644 examples-new/webhooks/config.json create mode 100644 examples-new/webhooks/requirements.txt create mode 100644 examples-new/webhooks/server.py create mode 100644 examples-new/webhooks/templates/base.html create mode 100644 examples-new/webhooks/templates/index.html diff --git a/examples-new/webhooks/README.md b/examples-new/webhooks/README.md new file mode 100644 index 00000000..d3c52a6d --- /dev/null +++ b/examples-new/webhooks/README.md @@ -0,0 +1,79 @@ +# 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: + +## 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 + +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 Example + +Run the example project 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! diff --git a/examples-new/webhooks/config.json b/examples-new/webhooks/config.json new file mode 100644 index 00000000..9b54e3df --- /dev/null +++ b/examples-new/webhooks/config.json @@ -0,0 +1,5 @@ +{ + "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-new/webhooks/requirements.txt b/examples-new/webhooks/requirements.txt new file mode 100644 index 00000000..dba38c7c --- /dev/null +++ b/examples-new/webhooks/requirements.txt @@ -0,0 +1,2 @@ +Flask>=0.11 +requests diff --git a/examples-new/webhooks/server.py b/examples-new/webhooks/server.py new file mode 100644 index 00000000..79f7d1d2 --- /dev/null +++ b/examples-new/webhooks/server.py @@ -0,0 +1,157 @@ +# 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.contrib.fixers import ProxyFix +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) + + +@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"]: + # 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.fromtimestamp(delta["date"]), + "object_id": delta["object_data"]["id"], + } + print(" * {type} at {date} with ID {object_id}".format(**kwargs)) + + # Finally, we have to return a 200 Success response to Nylas, to let them + # know that we processed the webhook successfully. + return "Success", 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 digest == signature + + +@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, run the Flask web server. +if __name__ == "__main__": + 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)) + app.run() diff --git a/examples-new/webhooks/templates/base.html b/examples-new/webhooks/templates/base.html new file mode 100644 index 00000000..3d6ac8a3 --- /dev/null +++ b/examples-new/webhooks/templates/base.html @@ -0,0 +1,14 @@ + + + + + Nylas Webhook Example + + + +
+

Nylas Webhook Example

+ {% block body %}{% endblock %} +
+ + diff --git a/examples-new/webhooks/templates/index.html b/examples-new/webhooks/templates/index.html new file mode 100644 index 00000000..93aa89ce --- /dev/null +++ b/examples-new/webhooks/templates/index.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block body %} +

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") }} +

+{% endblock %} From 0c0661054184de0a255c09a2f4b3043a73f5699d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 4 Aug 2017 09:36:55 -0400 Subject: [PATCH 104/451] Add celery to webhook example --- examples-new/webhooks/README.md | 48 +++++++++++++++- examples-new/webhooks/config.json | 4 +- examples-new/webhooks/requirements.txt | 1 + examples-new/webhooks/server.py | 76 +++++++++++++++++++------- 4 files changed, 108 insertions(+), 21 deletions(-) mode change 100644 => 100755 examples-new/webhooks/server.py diff --git a/examples-new/webhooks/README.md b/examples-new/webhooks/README.md index d3c52a6d..ac9483a1 100644 --- a/examples-new/webhooks/README.md +++ b/examples-new/webhooks/README.md @@ -7,6 +7,23 @@ 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. @@ -21,6 +38,14 @@ 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, @@ -49,9 +74,25 @@ 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 -Run the example project like this: +While the Celery worker is running, open a new terminal window and run the +Flask web server, like this: ``` python server.py @@ -77,3 +118,8 @@ Then click the "Create Webhook" button to save. 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-new/webhooks/config.json b/examples-new/webhooks/config.json index 9b54e3df..04e9a645 100644 --- a/examples-new/webhooks/config.json +++ b/examples-new/webhooks/config.json @@ -1,5 +1,7 @@ { "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" + "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-new/webhooks/requirements.txt b/examples-new/webhooks/requirements.txt index dba38c7c..38e90172 100644 --- a/examples-new/webhooks/requirements.txt +++ b/examples-new/webhooks/requirements.txt @@ -1,2 +1,3 @@ Flask>=0.11 +celery[redis]>=4.0.0 requests diff --git a/examples-new/webhooks/server.py b/examples-new/webhooks/server.py old mode 100644 new mode 100755 index 79f7d1d2..9d4b77dd --- a/examples-new/webhooks/server.py +++ b/examples-new/webhooks/server.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python + # Imports from the Python standard library from __future__ import print_function import os @@ -12,6 +14,7 @@ import requests from flask import Flask, request, render_template from werkzeug.contrib.fixers import ProxyFix + from celery import Celery except ImportError: message = textwrap.dedent(""" You need to install the dependencies for this project. @@ -49,6 +52,12 @@ # 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(): @@ -81,24 +90,17 @@ def webhook(): # Let's find out what it says... data = request.get_json() for delta in data["deltas"]: - # 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.fromtimestamp(delta["date"]), - "object_id": delta["object_data"]["id"], - } - print(" * {type} at {date} with ID {object_id}".format(**kwargs)) - - # Finally, we have to return a 200 Success response to Nylas, to let them - # know that we processed the webhook successfully. - return "Success", 200 + # 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): @@ -113,6 +115,26 @@ def verify_signature(message, key, signature): return 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.fromtimestamp(delta["date"]), + "object_id": delta["object_data"]["id"], + } + print(" * {type} at {date} with ID {object_id}".format(**kwargs)) + + @app.route("/") def index(): """ @@ -141,8 +163,14 @@ def ngrok_url(): return secure_urls[0] -# When this file is executed, run the Flask web server. +# 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( @@ -154,4 +182,14 @@ def ngrok_url(): 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() From 6d1c6d4467ad99d2e6f9acf85e310007faf4542e Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 4 Aug 2017 09:40:54 -0400 Subject: [PATCH 105/451] Add information about ngrok web interface --- examples-new/webhooks/templates/index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/examples-new/webhooks/templates/index.html b/examples-new/webhooks/templates/index.html index 93aa89ce..5069360f 100644 --- a/examples-new/webhooks/templates/index.html +++ b/examples-new/webhooks/templates/index.html @@ -9,4 +9,11 @@

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 %} From e65494ae00682e3e95f23ee599be5cb4d5962fc6 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 18:16:54 -0400 Subject: [PATCH 106/451] Refactor responses into pytest fixture --- requirements-dev.txt | 2 - tests/conftest.py | 110 ++++++++++++++++-------------- tests/test_accounts.py | 4 +- tests/test_client.py | 30 ++++---- tests/test_drafts.py | 2 - tests/test_events.py | 1 - tests/test_folders.py | 3 - tests/test_labels.py | 2 - tests/test_messages.py | 15 ++-- tests/test_search.py | 5 +- tests/test_send_error_handling.py | 37 +++++----- tests/test_threads.py | 8 --- tox.ini | 4 +- 13 files changed, 98 insertions(+), 125 deletions(-) delete mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 208ec64c..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -responses diff --git a/tests/conftest.py b/tests/conftest.py index a33ee024..4f934a98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,13 +53,20 @@ def api_client(api_url): @pytest.fixture -def mock_save_draft(api_url): +def mocked_responses(): + rmock = responses.RequestsMock(assert_all_requests_are_fired=False) + with rmock: + yield rmock + + +@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" }) - responses.add( + mocked_responses.add( responses.POST, save_endpoint, content_type='application/json', @@ -70,7 +77,7 @@ def mock_save_draft(api_url): @pytest.fixture -def mock_account(api_url, account_id): +def mock_account(mocked_responses, api_url, account_id): response_body = json.dumps( { "account_id": account_id, @@ -83,7 +90,7 @@ def mock_account(api_url, account_id): "billing_state": "paid", } ) - responses.add( + mocked_responses.add( responses.GET, re.compile(api_url + '/account/?'), content_type='application/json', @@ -93,7 +100,7 @@ def mock_account(api_url, account_id): @pytest.fixture -def mock_accounts(api_url, account_id, app_id): +def mock_accounts(mocked_responses, api_url, account_id, app_id): response_body = json.dumps([ { "account_id": account_id, @@ -107,7 +114,7 @@ def mock_accounts(api_url, account_id, app_id): } ]) url_re = "{base}(/a/{app_id})?/accounts/?".format(base=api_url, app_id=app_id) - responses.add( + mocked_responses.add( responses.GET, re.compile(url_re), content_type='application/json', @@ -117,7 +124,7 @@ def mock_accounts(api_url, account_id, app_id): @pytest.fixture -def mock_folder_account(api_url, account_id): +def mock_folder_account(mocked_responses, api_url, account_id): response_body = json.dumps( { "email_address": "ben.bitdiddle1861@office365.com", @@ -129,7 +136,7 @@ def mock_folder_account(api_url, account_id): "organization_unit": "folder" } ) - responses.add( + mocked_responses.add( responses.GET, api_url + '/account', content_type='application/json', @@ -140,7 +147,7 @@ def mock_folder_account(api_url, account_id): @pytest.fixture -def mock_labels(api_url, account_id): +def mock_labels(mocked_responses, api_url, account_id): response_body = json.dumps([ { "display_name": "Important", @@ -179,7 +186,7 @@ def mock_labels(api_url, account_id): } ]) endpoint = re.compile(api_url + '/labels.*') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', @@ -189,7 +196,7 @@ def mock_labels(api_url, account_id): @pytest.fixture -def mock_label(api_url, account_id): +def mock_label(mocked_responses, api_url, account_id): response_body = json.dumps( { "display_name": "Important", @@ -199,8 +206,8 @@ def mock_label(api_url, account_id): "object": "label" } ) - url = api_url + '/labels/anuep8pe5ugmxrucchrzba2o8' - responses.add( + url = api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') + mocked_responses.add( responses.GET, url, content_type='application/json', @@ -210,7 +217,7 @@ def mock_label(api_url, account_id): @pytest.fixture -def mock_folder(api_url, account_id): +def mock_folder(mocked_responses, api_url, account_id): folder = { "display_name": "My Folder", "id": "anuep8pe5ug3xrupchwzba2o8", @@ -220,7 +227,7 @@ def mock_folder(api_url, account_id): } response_body = json.dumps(folder) url = api_url + '/folders/anuep8pe5ug3xrupchwzba2o8' - responses.add( + mocked_responses.add( responses.GET, url, content_type='application/json', @@ -234,7 +241,7 @@ def request_callback(request): folder.update(payload) return (200, {}, json.dumps(folder)) - responses.add_callback( + mocked_responses.add_callback( responses.PUT, url, content_type='application/json', @@ -243,7 +250,7 @@ def request_callback(request): @pytest.fixture -def mock_messages(api_url, account_id): +def mock_messages(mocked_responses, api_url, account_id): response_body = json.dumps([ { "id": "1234", @@ -290,16 +297,17 @@ def mock_messages(api_url, account_id): } ]) endpoint = re.compile(api_url + '/messages') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', status=200, - body=response_body) + body=response_body + ) @pytest.fixture -def mock_message(api_url, account_id): +def mock_message(mocked_responses, api_url, account_id): base_msg = { "id": "1234", "subject": "Test Message", @@ -326,20 +334,20 @@ def request_callback(request): return (200, {}, json.dumps(base_msg)) endpoint = re.compile(api_url + '/messages/1234') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', status=200, body=response_body ) - responses.add_callback( + mocked_responses.add_callback( responses.PUT, endpoint, content_type='application/json', callback=request_callback ) - responses.add( + mocked_responses.add( responses.DELETE, endpoint, content_type='application/json', @@ -349,7 +357,7 @@ def request_callback(request): @pytest.fixture -def mock_threads(api_url, account_id): +def mock_threads(mocked_responses, api_url, account_id): response_body = json.dumps([ { "id": "5678", @@ -366,7 +374,7 @@ def mock_threads(api_url, account_id): } ]) endpoint = re.compile(api_url + '/threads') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', @@ -376,7 +384,7 @@ def mock_threads(api_url, account_id): @pytest.fixture -def mock_thread(api_url, account_id): +def mock_thread(mocked_responses, api_url, account_id): base_thrd = { "id": "5678", "subject": "Test Thread", @@ -401,14 +409,14 @@ def request_callback(request): return (200, {}, json.dumps(base_thrd)) endpoint = re.compile(api_url + '/threads/5678') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', status=200, body=response_body ) - responses.add_callback( + mocked_responses.add_callback( responses.PUT, endpoint, content_type='application/json', @@ -417,7 +425,7 @@ def request_callback(request): @pytest.fixture -def mock_labelled_thread(api_url, account_id): +def mock_labelled_thread(mocked_responses, api_url, account_id): base_thread = { "id": "111", "subject": "Labelled Thread", @@ -472,14 +480,14 @@ def request_callback(request): return (200, {}, json.dumps(copied)) endpoint = re.compile(api_url + '/threads/111') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', status=200, body=response_body ) - responses.add_callback( + mocked_responses.add_callback( responses.PUT, endpoint, content_type='application/json', @@ -488,7 +496,7 @@ def request_callback(request): @pytest.fixture -def mock_drafts(api_url): +def mock_drafts(mocked_responses, api_url): response_body = json.dumps([{ "bcc": [], "body": "Cheers mate!", @@ -517,7 +525,7 @@ def mock_drafts(api_url): "version": 0 }]) - responses.add( + mocked_responses.add( responses.GET, api_url + '/drafts', content_type='application/json', @@ -527,7 +535,7 @@ def mock_drafts(api_url): @pytest.fixture -def mock_draft_saved_response(api_url): +def mock_draft_saved_response(mocked_responses, api_url): draft_json = { "bcc": [], "body": "Cheers mate!", @@ -569,14 +577,14 @@ def request_callback(request): updated_draft_json.update(stripped_payload) return (200, {}, json.dumps(updated_draft_json)) - responses.add_callback( + mocked_responses.add_callback( responses.POST, api_url + '/drafts/', content_type='application/json', callback=request_callback, ) - responses.add_callback( + mocked_responses.add_callback( responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', @@ -585,8 +593,8 @@ def request_callback(request): @pytest.fixture -def mock_draft_deleted_response(api_url): - responses.add( +def mock_draft_deleted_response(mocked_responses, api_url): + mocked_responses.add( responses.DELETE, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', @@ -596,7 +604,7 @@ def mock_draft_deleted_response(api_url): @pytest.fixture -def mock_draft_sent_response(api_url): +def mock_draft_sent_response(mocked_responses, api_url): body = { "bcc": [], "body": "", @@ -634,7 +642,7 @@ def callback(request): assert payload['version'] == 0 return values.pop() - responses.add_callback( + mocked_responses.add_callback( responses.POST, api_url + '/send/', callback=callback, @@ -693,7 +701,7 @@ def mock_event_create_notify_response(api_url, message_body): @pytest.fixture -def mock_thread_search_response(api_url): +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" @@ -735,7 +743,7 @@ def mock_thread_search_response(api_url): } ]) - responses.add( + mocked_responses.add( responses.GET, api_url + '/threads/search?q=Helena', body=response_body, @@ -745,7 +753,7 @@ def mock_thread_search_response(api_url): ) @pytest.fixture -def mock_message_search_response(api_url): +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. :) " @@ -822,7 +830,7 @@ def mock_message_search_response(api_url): } ]) - responses.add( + mocked_responses.add( responses.GET, api_url + '/messages/search?q=Pinot', body=response_body, @@ -833,7 +841,7 @@ def mock_message_search_response(api_url): @pytest.fixture -def mock_calendars(api_url): +def mock_calendars(mocked_responses, api_url): response_body = json.dumps([ { "id": "8765", @@ -852,7 +860,7 @@ def mock_calendars(api_url): } ]) endpoint = re.compile(api_url + '/calendars') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', @@ -861,7 +869,7 @@ def mock_calendars(api_url): ) @pytest.fixture -def mock_events(api_url): +def mock_events(mocked_responses, api_url): response_body = json.dumps([ { "title": "Pool party", @@ -875,7 +883,7 @@ def mock_events(api_url): } ]) endpoint = re.compile(api_url + '/events') - responses.add( + mocked_responses.add( responses.GET, endpoint, content_type='application/json', @@ -885,7 +893,7 @@ def mock_events(api_url): @pytest.fixture -def mock_account_management(api_url, account_id, app_id): +def mock_account_management(mocked_responses, api_url, account_id, app_id): account = { "account_id": account_id, "email_address": "ben.bitdiddle1861@gmail.com", @@ -906,14 +914,14 @@ def mock_account_management(api_url, account_id, app_id): downgrade_url = "{base}/a/{app_id}/accounts/{id}/downgrade".format( base=api_url, id=account_id, app_id=app_id, ) - responses.add( + mocked_responses.add( responses.POST, upgrade_url, content_type='application/json', status=200, body=paid_response, ) - responses.add( + mocked_responses.add( responses.POST, downgrade_url, content_type='application/json', diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 6714ef8b..702d3274 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -22,7 +22,6 @@ def test_account_json(api_client, monkeypatch): assert isinstance(result, dict) -@responses.activate @pytest.mark.usefixtures("mock_accounts", "mock_account_management") def test_account_upgrade(api_client, app_id): api_client.app_id = app_id @@ -41,9 +40,8 @@ def test_account_delete(api_client, monkeypatch): account.delete() -@responses.activate @pytest.mark.usefixtures("mock_accounts", "mock_account") -def test_account_access(api_client): +def test_account_access(api_client, mocked_responses): account1 = api_client.account assert isinstance(account1, SingletonAccount) account2 = api_client.accounts[0] diff --git a/tests/test_client.py b/tests/test_client.py index ea4831d3..2adce17a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -90,11 +90,10 @@ def test_client_authentication_url(api_client, api_url): assert urls_equal(expected3, actual3) -@responses.activate -def test_client_token_for_code(api_client, api_url): +def test_client_token_for_code(mocked_responses, api_client, api_url): endpoint = re.compile(api_url + '/oauth/token') response_body = json.dumps({"access_token": "hooray"}) - responses.add( + mocked_responses.add( responses.POST, endpoint, content_type='application/json', @@ -103,8 +102,8 @@ def test_client_token_for_code(api_client, api_url): ) assert api_client.token_for_code("foo") == "hooray" - assert len(responses.calls) == 1 - request = responses.calls[0].request + assert len(mocked_responses.calls) == 1 + request = mocked_responses.calls[0].request body = parse_qs(request.body) assert body["grant_type"] == ["authorization_code"] assert body["code"] == ["foo"] @@ -120,10 +119,9 @@ def test_client_opensource_api(api_client): assert api_client.is_opensource_api() == True -@responses.activate -def test_client_revoke_token(api_client, api_url): +def test_client_revoke_token(mocked_responses, api_client, api_url): endpoint = re.compile(api_url + '/oauth/revoke') - responses.add( + mocked_responses.add( responses.POST, endpoint, status=200, @@ -135,11 +133,10 @@ def test_client_revoke_token(api_client, api_url): api_client.revoke_token() assert api_client.auth_token is None assert api_client.access_token is None - assert len(responses.calls) == 1 + assert len(mocked_responses.calls) == 1 -@responses.activate -def test_create_resources(api_client, api_url): +def test_create_resources(mocked_responses, api_client, api_url): contacts_data = [ { "id": 1, @@ -151,7 +148,7 @@ def test_create_resources(api_client, api_url): "email": "second@example.com", } ] - responses.add( + mocked_responses.add( responses.POST, api_url + "/contacts/", content_type='application/json', @@ -166,17 +163,16 @@ def test_create_resources(api_client, api_url): contacts = api_client._create_resources(Contact, post_data) assert len(contacts) == 2 assert all(isinstance(contact, Contact) for contact in contacts) - assert len(responses.calls) == 1 + assert len(mocked_responses.calls) == 1 -@responses.activate -def test_call_resource_method(api_client, api_url): +def test_call_resource_method(mocked_responses, api_client, api_url): contact_data = { "id": 1, "name": "first", "email": "first@example.com", } - responses.add( + mocked_responses.add( responses.POST, api_url + "/contacts/1/remove_duplicates", content_type='application/json', @@ -188,4 +184,4 @@ def test_call_resource_method(api_client, api_url): Contact, 1, "remove_duplicates", {} ) assert isinstance(contact, Contact) - assert len(responses.calls) == 1 + assert len(mocked_responses.calls) == 1 diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 6cfe89fc..1adfaa9d 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -5,7 +5,6 @@ # pylint: disable=len-as-condition -@responses.activate @pytest.mark.usefixtures( "mock_draft_saved_response", "mock_draft_sent_response" ) @@ -52,7 +51,6 @@ def test_draft_attachment(api_client): assert len(draft.file_ids) == 0 -@responses.activate @pytest.mark.usefixtures( "mock_draft_saved_response", "mock_draft_deleted_response" ) diff --git a/tests/test_events.py b/tests/test_events.py index 30b89a77..e2bc797b 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -41,7 +41,6 @@ def test_event_notify(api_client): assert query['other_param'][0] == '1' -@responses.activate @pytest.mark.usefixtures("mock_calendars", "mock_events") def test_calendar_events(api_client): calendar = api_client.calendars.first() diff --git a/tests/test_folders.py b/tests/test_folders.py index 407c0d30..3d56f46c 100644 --- a/tests/test_folders.py +++ b/tests/test_folders.py @@ -3,7 +3,6 @@ from nylas.client.restful_models import Folder, Thread, Message -@responses.activate @pytest.mark.usefixtures("mock_folder") def test_get_change_folder(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') @@ -15,7 +14,6 @@ def test_get_change_folder(api_client): assert folder.display_name == 'My New Folder' -@responses.activate @pytest.mark.usefixtures("mock_folder", "mock_threads") def test_folder_threads(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') @@ -24,7 +22,6 @@ def test_folder_threads(api_client): for thread in folder.threads) -@responses.activate @pytest.mark.usefixtures("mock_folder", "mock_messages") def test_folder_messages(api_client): folder = api_client.folders.find('anuep8pe5ug3xrupchwzba2o8') diff --git a/tests/test_labels.py b/tests/test_labels.py index 74cf02e9..0588c1d8 100644 --- a/tests/test_labels.py +++ b/tests/test_labels.py @@ -3,7 +3,6 @@ from nylas.client.restful_models import Label, Thread, Message -@responses.activate @pytest.mark.usefixtures("mock_labels") def test_list_labels(api_client): labels = api_client.labels @@ -12,7 +11,6 @@ def test_list_labels(api_client): assert all(isinstance(x, Label) for x in labels) -@responses.activate @pytest.mark.usefixtures("mock_label") def test_get_label(api_client): label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') diff --git a/tests/test_messages.py b/tests/test_messages.py index 2a498c10..8a76136e 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,12 +1,10 @@ import json import six import pytest -import responses from urlobject import URLObject from nylas.client.restful_models import Message -@responses.activate @pytest.mark.usefixtures("mock_messages") def test_messages(api_client): message = api_client.messages.first() @@ -17,7 +15,6 @@ def test_messages(api_client): assert not message.starred -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") def test_message_stars(api_client): message = api_client.messages.first() @@ -28,7 +25,6 @@ def test_message_stars(api_client): assert message.starred is False -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") def test_message_read(api_client): message = api_client.messages.first() @@ -41,7 +37,7 @@ def test_message_read(api_client): message.mark_as_seen() assert message.unread is False -@responses.activate + @pytest.mark.usefixtures("mock_account", "mock_messages", "mock_message") def test_message_labels(api_client): message = api_client.messages.first() @@ -59,7 +55,6 @@ def test_message_labels(api_client): assert message.folder is None -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_message", "mock_messages") def test_message_raw(api_client, account_id): message = api_client.messages.first() @@ -80,17 +75,15 @@ def test_message_raw(api_client, account_id): } -@responses.activate @pytest.mark.usefixtures("mock_message") -def test_message_delete_by_id(api_client): +def test_message_delete_by_id(mocked_responses, api_client): api_client.messages.delete(1234, forceful=True) - assert len(responses.calls) == 1 - request = responses.calls[0].request + assert len(mocked_responses.calls) == 1 + request = mocked_responses.calls[0].request url = URLObject(request.url) assert url.query_dict["forceful"] == "True" -@responses.activate @pytest.mark.usefixtures("mock_messages") def test_slice_messages(api_client): messages = api_client.messages[0:2] diff --git a/tests/test_search.py b/tests/test_search.py index 0d084bf7..e83dbb5c 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -2,14 +2,13 @@ import responses -@responses.activate @pytest.mark.usefixtures("mock_thread_search_response") def test_search_threads(api_client): threads = api_client.threads.search("Helena") assert len(threads) == 1 assert "Helena" in threads[0].snippet -@responses.activate + @pytest.mark.usefixtures("mock_message_search_response") def test_search_messages(api_client): messages = api_client.messages.search("Pinot") @@ -17,7 +16,7 @@ def test_search_messages(api_client): assert "Pinot" in messages[0].snippet assert "Pinot" in messages[1].snippet -@responses.activate + @pytest.mark.usefixtures("mock_message_search_response") def test_search_drafts(api_client): with pytest.raises(Exception): diff --git a/tests/test_send_error_handling.py b/tests/test_send_error_handling.py index 96111b69..916294ab 100644 --- a/tests/test_send_error_handling.py +++ b/tests/test_send_error_handling.py @@ -8,7 +8,7 @@ ) -def mock_sending_error(http_code, message, api_url, server_error=None): +def mock_sending_error(http_code, message, mocked_responses, api_url, server_error=None): send_endpoint = re.compile(api_url + '/send') response_body = { "type": "api_error", @@ -23,51 +23,51 @@ def mock_sending_error(http_code, message, api_url, server_error=None): response_body['server_error'] = server_error response_body = json.dumps(response_body) - responses.add(responses.POST, send_endpoint, - content_type='application/json', status=http_code, - body=response_body) + mocked_responses.add( + responses.POST, + send_endpoint, + content_type='application/json', + status=http_code, + body=response_body, + ) -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_message_rejected(api_client, api_url): +def test_handle_message_rejected(mocked_responses, api_client, api_url): draft = api_client.drafts.create() error_message = 'Sending to all recipients failed' - mock_sending_error(402, error_message, api_url=api_url) + mock_sending_error(402, error_message, mocked_responses, api_url=api_url) with pytest.raises(MessageRejectedError) as exc: draft.send() assert exc.value.message == error_message -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_quota_exceeded(api_client, api_url): +def test_handle_quota_exceeded(mocked_responses, api_client, api_url): draft = api_client.drafts.create() error_message = 'Daily sending quota exceeded' - mock_sending_error(429, error_message, api_url=api_url) + mock_sending_error(429, error_message, mocked_responses, api_url=api_url) with pytest.raises(SendingQuotaExceededError) as exc: draft.send() assert exc.value.message == error_message -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_handle_service_unavailable(api_client, api_url): +def test_handle_service_unavailable(mocked_responses, api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' - mock_sending_error(503, error_message, api_url=api_url) + mock_sending_error(503, error_message, mocked_responses, api_url=api_url) with pytest.raises(ServiceUnavailableError) as exc: draft.send() assert exc.value.message == error_message -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_returns_server_error(api_client, api_url): +def test_returns_server_error(mocked_responses, api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' reason = 'Rejected potential SPAM' - mock_sending_error(503, error_message, api_url=api_url, + mock_sending_error(503, error_message, mocked_responses, api_url=api_url, server_error=reason) with pytest.raises(ServiceUnavailableError) as exc: draft.send() @@ -76,12 +76,11 @@ def test_returns_server_error(api_client, api_url): assert exc.value.server_error == reason -@responses.activate @pytest.mark.usefixtures("mock_account", "mock_save_draft") -def test_doesnt_return_server_error_if_not_defined(api_client, api_url): +def test_doesnt_return_server_error_if_not_defined(mocked_responses, api_client, api_url): draft = api_client.drafts.create() error_message = 'The server unexpectedly closed the connection' - mock_sending_error(503, error_message, api_url=api_url) + mock_sending_error(503, error_message, mocked_responses, api_url=api_url) with pytest.raises(ServiceUnavailableError) as exc: draft.send() assert exc.value.message == error_message diff --git a/tests/test_threads.py b/tests/test_threads.py index 1b75eca2..4bc6c20d 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -3,7 +3,6 @@ from nylas.client.restful_models import Message, Draft, Label -@responses.activate @pytest.mark.usefixtures("mock_threads") def test_thread_folder(api_client): thread = api_client.threads.first() @@ -14,7 +13,6 @@ def test_thread_folder(api_client): assert thread.starred -@responses.activate @pytest.mark.usefixtures("mock_folder_account", "mock_threads", "mock_thread") def test_thread_change(api_client): thread = api_client.threads.first() @@ -30,7 +28,6 @@ def test_thread_change(api_client): assert thread.folders[0].id == 'qwer' -@responses.activate @pytest.mark.usefixtures("mock_threads", "mock_messages") def test_thread_messages(api_client): thread = api_client.threads.first() @@ -39,7 +36,6 @@ def test_thread_messages(api_client): for message in thread.messages) -@responses.activate @pytest.mark.usefixtures("mock_threads", "mock_drafts") def test_thread_drafts(api_client): thread = api_client.threads.first() @@ -48,7 +44,6 @@ def test_thread_drafts(api_client): for draft in thread.drafts) -@responses.activate @pytest.mark.usefixtures("mock_labelled_thread", "mock_labels") def test_thread_label(api_client): thread = api_client.threads.find(111) @@ -65,7 +60,6 @@ def test_thread_label(api_client): assert thread.labels == returned -@responses.activate @pytest.mark.usefixtures("mock_labelled_thread", "mock_labels") def test_thread_labels(api_client): thread = api_client.threads.find(111) @@ -83,7 +77,6 @@ def test_thread_labels(api_client): assert thread.labels == returned -@responses.activate @pytest.mark.usefixtures("mock_threads", "mock_thread") def test_thread_read(api_client): thread = api_client.threads.first() @@ -99,7 +92,6 @@ def test_thread_read(api_client): assert thread.unread is False -@responses.activate @pytest.mark.usefixtures("mock_threads") def test_thread_reply(api_client): thread = api_client.threads.first() diff --git a/tox.ini b/tox.ini index f8380a3e..80e89c02 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,5 @@ envlist = py27,pypy,py34 [testenv] commands = - pip install -e . + pip install -e .[test] pytest -deps = - -rrequirements-dev.txt From 0a9e38a28f477c6574f51781faffaa5eb5ed8301 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 18:45:14 -0400 Subject: [PATCH 107/451] Remove httpretty Replace with responses --- setup.py | 1 - tests/conftest.py | 59 +++++++++++++------------ tests/test_events.py | 11 ++--- tests/test_filter.py | 102 +++++++++++++++++++------------------------ 4 files changed, 82 insertions(+), 91 deletions(-) diff --git a/setup.py b/setup.py index 9cc37caf..7b6e7f7f 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ "pytest-cov", "pytest-pylint", "responses", - "httpretty", ] diff --git a/tests/conftest.py b/tests/conftest.py index 4f934a98..6e5ed46d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ import copy import pytest import responses -import httpretty from nylas import APIClient # pylint: disable=redefined-outer-name @@ -651,8 +650,7 @@ def callback(request): @pytest.fixture -def mock_files(api_url): - httpretty.enable() +def mock_files(mocked_responses, api_url): body = [{ "content_type": "text/plain", "filename": "a.txt", @@ -661,41 +659,46 @@ def mock_files(api_url): "object": "file", "size": 762878 }] + mocked_responses.add( + responses.POST, + api_url + '/files/', + body=json.dumps(body), + ) + mocked_responses.add( + responses.GET, + api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', + body='test body', + ) - values = [httpretty.Response(status=200, body=json.dumps(body))] - httpretty.register_uri(httpretty.POST, api_url + '/files/', responses=values) - httpretty.register_uri(httpretty.GET, api_url + '/files/3qfe4k3siosfjtjpfdnon8zbn/download', - body='test body') @pytest.fixture -def mock_event_create_response(api_url, message_body): - httpretty.enable() - values = [ - httpretty.Response(status=200, body=json.dumps(message_body)), - httpretty.Response(status=400, body=''), - ] - - httpretty.register_uri(httpretty.POST, api_url + '/events/', responses=values) - - body = json.dumps({'title': 'loaded from JSON', 'ignored': 'ignored'}) - put_values = [ - httpretty.Response(status=200, body=body) - ] - httpretty.register_uri( - httpretty.PUT, +def mock_event_create_response(mocked_responses, api_url, message_body): + values = [(400, {}, ""), + (200, {}, json.dumps(message_body))] + + def callback(request): + return values.pop() + + 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', - responses=put_values, + body=json.dumps(put_body) ) @pytest.fixture -def mock_event_create_notify_response(api_url, message_body): - httpretty.enable() - httpretty.register_uri( - httpretty.POST, +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), - status=200 ) diff --git a/tests/test_events.py b/tests/test_events.py index e2bc797b..19f4a95c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,6 +1,6 @@ import pytest -import httpretty import responses +from urlobject import URLObject from nylas.client.errors import InvalidRequestError from nylas.client.restful_models import Event @@ -31,14 +31,15 @@ def test_event_crud(api_client): @pytest.mark.usefixtures("mock_event_create_notify_response") -def test_event_notify(api_client): +def test_event_notify(mocked_responses, api_client): event1 = blank_event(api_client) event1.save(notify_participants='true', other_param='1') assert event1.id == 'cv4ei7syx10uvsxbs21ccsezf' - query = httpretty.last_request().querystring - assert query['notify_participants'][0] == 'true' - assert query['other_param'][0] == '1' + url = mocked_responses.calls[-1].request.url + query = URLObject(url).query_dict + assert query['notify_participants'] == 'true' + assert query['other_param'] == '1' @pytest.mark.usefixtures("mock_calendars", "mock_events") diff --git a/tests/test_filter.py b/tests/test_filter.py index fc3c263b..beca22a6 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -1,91 +1,79 @@ import json import random -import httpretty -from httpretty import Response +import responses +from urlobject import URLObject -def test_no_filter(api_client, api_url, message_body): - httpretty.enable() - +def test_no_filter(mocked_responses, api_client, api_url, message_body): message_body_list_50 = [message_body for _ in range(1, 51)] message_body_list_22 = [message_body for _ in range(1, 23)] - # httpretty kind of sucks and strips & parameters from the URL values = [ - Response(status=200, body=json.dumps(message_body_list_50)), - Response(status=200, body=json.dumps(message_body_list_22)), + (200, {}, json.dumps(message_body_list_22)), + (200, {}, json.dumps(message_body_list_50)), ] - httpretty.register_uri( - httpretty.GET, + + def callback(request): + return values.pop() + + mocked_responses.add_callback( + responses.GET, api_url + '/events', - responses=values, + callback=callback, ) events = api_client.events.all() assert len(events) == 72 assert events[0].id == 'cv4ei7syx10uvsxbs21ccsezf' - httpretty.disable() - - -def test_two_filters(api_client, api_url): - httpretty.enable() - values2 = [Response(status=200, body='[]')] - httpretty.register_uri( - httpretty.GET, +def test_two_filters(mocked_responses, api_client, api_url): + mocked_responses.add( + responses.GET, api_url + '/events?param1=a¶m2=b', - responses=values2, + body='[]', ) events = api_client.events.where(param1='a', param2='b').all() assert len(events) == 0 # pylint: disable=len-as-condition - query = httpretty.last_request().querystring - assert query['param1'][0] == 'a' - assert query['param2'][0] == 'b' - httpretty.disable() + url = mocked_responses.calls[-1].request.url + query = URLObject(url).query_dict + assert query['param1'] == 'a' + assert query['param2'] == 'b' -def test_no_offset(api_client, api_url): - httpretty.enable() - - values = [Response(status=200, body='[]')] - httpretty.register_uri( - httpretty.GET, +def test_no_offset(mocked_responses, api_client, api_url): + mocked_responses.add( + responses.GET, api_url + '/events?in=Nylas', - responses=values, + body='[]', ) list(api_client.events.where({'in': 'Nylas'}).items()) - query = httpretty.last_request().querystring - assert query['in'][0] == 'Nylas' - assert query['offset'][0] == '0' - httpretty.disable() - -def test_zero_offset(api_client, api_url): - httpretty.enable() + url = mocked_responses.calls[-1].request.url + query = URLObject(url).query_dict + assert query['in'] == 'Nylas' + assert query['offset'] == '0' - values = [Response(status=200, body='[]')] - httpretty.register_uri( - httpretty.GET, +def test_zero_offset(mocked_responses, api_client, api_url): + mocked_responses.add( + responses.GET, api_url + '/events?in=Nylas&offset=0', - responses=values, + body='[]', ) list(api_client.events.where({'in': 'Nylas', 'offset': 0}).items()) - query = httpretty.last_request().querystring - assert query['in'][0] == 'Nylas' - assert query['offset'][0] == '0' - httpretty.disable() - -def test_non_zero_offset(api_client, api_url): - httpretty.enable() + url = mocked_responses.calls[-1].request.url + query = URLObject(url).query_dict + assert query['in'] == 'Nylas' + assert query['offset'] == '0' +def test_non_zero_offset(mocked_responses, api_client, api_url): offset = random.randint(1, 1000) - values = [Response(status=200, body='[]')] - httpretty.register_uri( - httpretty.GET, + mocked_responses.add( + responses.GET, api_url + '/events?in=Nylas&offset=' + str(offset), - responses=values, + body='[]', ) + list(api_client.events.where({'in': 'Nylas', 'offset': offset}).items()) - query = httpretty.last_request().querystring - assert query['in'][0] == 'Nylas' - assert query['offset'][0] == str(offset) - httpretty.disable() + url = mocked_responses.calls[-1].request.url + query = URLObject(url).query_dict + assert query['in'] == 'Nylas' + assert query['offset'] == str(offset) From f2ec92c9e9d827a5ae307df5ac4146beb6ce43a7 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 18:57:54 -0400 Subject: [PATCH 108/451] fix pylint --- nylas/client/client.py | 2 +- tests/conftest.py | 2 +- tests/test_accounts.py | 3 +-- tests/test_drafts.py | 1 - tests/test_events.py | 1 - tests/test_filter.py | 2 +- tests/test_folders.py | 1 - tests/test_labels.py | 3 --- tests/test_search.py | 1 - tests/test_threads.py | 1 - 10 files changed, 4 insertions(+), 13 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index b41f782e..91de992c 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -377,7 +377,7 @@ def _call_resource_method(self, cls, id, method_name, data): for example /a/.../accounts/id/upgrade""" if cls.api_root != 'a': - url_path = "/{name}/{id}/{method}".format( + url_path = "/{name}/{id}/{method}".format( name=cls.collection_name, id=id, method=method_name ) else: diff --git a/tests/conftest.py b/tests/conftest.py index 6e5ed46d..9c08ab99 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -676,7 +676,7 @@ def mock_event_create_response(mocked_responses, api_url, message_body): values = [(400, {}, ""), (200, {}, json.dumps(message_body))] - def callback(request): + def callback(_request): return values.pop() mocked_responses.add_callback( diff --git a/tests/test_accounts.py b/tests/test_accounts.py index 702d3274..289a942d 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -1,5 +1,4 @@ import pytest -import responses from nylas.client.restful_models import Account, APIAccount, SingletonAccount @@ -41,7 +40,7 @@ def test_account_delete(api_client, monkeypatch): @pytest.mark.usefixtures("mock_accounts", "mock_account") -def test_account_access(api_client, mocked_responses): +def test_account_access(api_client): account1 = api_client.account assert isinstance(account1, SingletonAccount) account2 = api_client.accounts[0] diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 1adfaa9d..f8747c3a 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -1,5 +1,4 @@ import pytest -import responses from nylas.client.errors import InvalidRequestError # pylint: disable=len-as-condition diff --git a/tests/test_events.py b/tests/test_events.py index 19f4a95c..8aca73be 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,5 +1,4 @@ import pytest -import responses from urlobject import URLObject from nylas.client.errors import InvalidRequestError from nylas.client.restful_models import Event diff --git a/tests/test_filter.py b/tests/test_filter.py index beca22a6..a1e67b0b 100644 --- a/tests/test_filter.py +++ b/tests/test_filter.py @@ -13,7 +13,7 @@ def test_no_filter(mocked_responses, api_client, api_url, message_body): (200, {}, json.dumps(message_body_list_50)), ] - def callback(request): + def callback(_request): return values.pop() mocked_responses.add_callback( diff --git a/tests/test_folders.py b/tests/test_folders.py index 3d56f46c..dfc4e9e8 100644 --- a/tests/test_folders.py +++ b/tests/test_folders.py @@ -1,5 +1,4 @@ import pytest -import responses from nylas.client.restful_models import Folder, Thread, Message diff --git a/tests/test_labels.py b/tests/test_labels.py index 0588c1d8..823028d8 100644 --- a/tests/test_labels.py +++ b/tests/test_labels.py @@ -1,5 +1,4 @@ import pytest -import responses from nylas.client.restful_models import Label, Thread, Message @@ -19,7 +18,6 @@ def test_get_label(api_client): assert label.display_name == 'Important' -@responses.activate @pytest.mark.usefixtures("mock_label", "mock_threads") def test_label_threads(api_client): label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') @@ -28,7 +26,6 @@ def test_label_threads(api_client): for thread in label.threads) -@responses.activate @pytest.mark.usefixtures("mock_label", "mock_messages") def test_label_messages(api_client): label = api_client.labels.find('anuep8pe5ugmxrucchrzba2o8') diff --git a/tests/test_search.py b/tests/test_search.py index e83dbb5c..bdac4b32 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,5 +1,4 @@ import pytest -import responses @pytest.mark.usefixtures("mock_thread_search_response") diff --git a/tests/test_threads.py b/tests/test_threads.py index 4bc6c20d..2abf27f8 100644 --- a/tests/test_threads.py +++ b/tests/test_threads.py @@ -1,5 +1,4 @@ import pytest -import responses from nylas.client.restful_models import Message, Draft, Label From a54e1c1ab32a54504ecd6db4c3fcf9f91034dce9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Fri, 4 Aug 2017 09:54:16 -0400 Subject: [PATCH 109/451] Fix a rebase mistake --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9c08ab99..3bdd6eff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -205,7 +205,7 @@ def mock_label(mocked_responses, api_url, account_id): "object": "label" } ) - url = api_url + '/labels/anuep8pe5ugmxrucchrzba2o8') + url = api_url + '/labels/anuep8pe5ugmxrucchrzba2o8' mocked_responses.add( responses.GET, url, From 14d50fc7412db4c9cf03ceb94ec0b6e5d8fe737c Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 16:02:00 -0400 Subject: [PATCH 110/451] Make error message consistent with check --- nylas/client/client.py | 2 +- tests/conftest.py | 2 +- tests/test_client.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 91de992c..ea9f2b49 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -90,7 +90,7 @@ def __init__(self, app_id=environ.get('NYLAS_APP_ID'), app_secret=environ.get('NYLAS_APP_SECRET'), access_token=environ.get('NYLAS_ACCESS_TOKEN'), api_server=API_SERVER): - if "://" not in api_server: + if not api_server.startswith("https://"): raise Exception("When overriding the Nylas API server address, you" " must include https://") self.api_server = api_server diff --git a/tests/conftest.py b/tests/conftest.py index 3bdd6eff..af96ba90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ def message_body(): @pytest.fixture def api_url(): - return 'http://localhost:2222' + return 'https://localhost:2222' @pytest.fixture diff --git a/tests/test_client.py b/tests/test_client.py index 2adce17a..2e7a75b9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -21,8 +21,8 @@ def urls_equal(url1, url2): def test_custom_client(): # Can specify API server - custom = APIClient(api_server="http://example.com") - assert custom.api_server == "http://example.com" + custom = APIClient(api_server="https://example.com") + assert custom.api_server == "https://example.com" # Must be a valid URL with pytest.raises(Exception) as exc: APIClient(api_server="invalid") From 9a0fb908c3d8f7c0bbd28b04828e5ad4f97f46ce Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 15:48:47 -0400 Subject: [PATCH 111/451] Refactor how client validates responses to better utilize the `requests` module --- nylas/client/client.py | 85 +++++++++++++++++++----------------------- nylas/client/errors.py | 15 ++++++++ tests/test_client.py | 24 ++++++++++++ 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index 91de992c..adf53414 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -13,64 +13,57 @@ Account, APIAccount, SingletonAccount, Folder, Label, Draft ) -from nylas.client.errors import ( - APIClientError, ConnectionError, NotAuthorizedError, - InvalidRequestError, NotFoundError, MethodNotSupportedError, - ServerError, ServiceUnavailableError, ConflictError, - SendingQuotaExceededError, ServerTimeoutError, MessageRejectedError -) +from nylas.client.errors import APIClientError, ConnectionError, STATUS_MAP DEBUG = environ.get('NYLAS_CLIENT_DEBUG') API_SERVER = "https://api.nylas.com" def _validate(response): - status_code_to_exc = {400: InvalidRequestError, - 401: NotAuthorizedError, - 402: MessageRejectedError, - 403: NotAuthorizedError, - 404: NotFoundError, - 405: MethodNotSupportedError, - 409: ConflictError, - 429: SendingQuotaExceededError, - 500: ServerError, - 503: ServiceUnavailableError, - 504: ServerTimeoutError} - request = response.request - url = request.url - status_code = response.status_code - data = request.body - if DEBUG: # pragma: no cover - print("{} {} ({}) => {}: {}".format(request.method, url, data, - status_code, response.text)) - - try: - data = json.loads(data) if data else None - except (ValueError, TypeError): - pass - - if status_code == 200: + print("{method} {url} ({body}) => {status}: {text}".format( + method=response.request.method, + url=response.request.url, + data=response.request.body, + status=response.status_code, + text=response.text, + )) + + if response.ok: return response - elif status_code in status_code_to_exc: - cls = status_code_to_exc[status_code] - try: - response = json.loads(response.text) - kwargs = dict(url=url, status_code=status_code, - data=data) - for key in ['message', 'server_error']: - if key in response: - kwargs[key] = response[key] + # The rest of this function is logic for raising the correct exception + # from the `nylas.client.errors` module. In the future, it may be worth changing + # this function to just call `response.raise_for_status()`. + # http://docs.python-requests.org/en/master/api/#requests.Response.raise_for_status + try: + data = response.json() + json_content = True + except json.JSONDecodeError: + data = response.content + json_content = False + + kwargs = { + "url": response.request.url, + "status_code": response.status_code, + "data": data, + } + + if response.status_code in STATUS_MAP: + cls = STATUS_MAP[response.status_code] + if json_content: + if "message" in data: + kwargs["message"] = data["message"] + if "server_error" in data: + kwargs["server_error"] = data["server_error"] + raise cls(**kwargs) + else: + kwargs["message"] = "Malformed" raise cls(**kwargs) - - except (ValueError, TypeError): - raise cls(url=url, status_code=status_code, - data=data, message="Malformed") else: - raise APIClientError(url=url, status_code=status_code, - data=data, message="Unknown status code.") + kwargs["message"] = "Unknown status code." + raise APIClientError(**kwargs) def nylas_excepted(func): diff --git a/nylas/client/errors.py b/nylas/client/errors.py index d5641547..29dc3f05 100644 --- a/nylas/client/errors.py +++ b/nylas/client/errors.py @@ -68,3 +68,18 @@ class ServerTimeoutError(APIClientError): class FileUploadError(APIClientError): pass + + +STATUS_MAP = { + 400: InvalidRequestError, + 401: NotAuthorizedError, + 402: MessageRejectedError, + 403: NotAuthorizedError, + 404: NotFoundError, + 405: MethodNotSupportedError, + 409: ConflictError, + 429: SendingQuotaExceededError, + 500: ServerError, + 503: ServiceUnavailableError, + 504: ServerTimeoutError, +} diff --git a/tests/test_client.py b/tests/test_client.py index 2adce17a..d73ea2e9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -184,4 +184,28 @@ def test_call_resource_method(mocked_responses, api_client, api_url): Contact, 1, "remove_duplicates", {} ) assert isinstance(contact, Contact) +<<<<<<< HEAD assert len(mocked_responses.calls) == 1 +======= + assert len(responses.calls) == 1 + + +@responses.activate +def test_201_response(api_client, api_url): + contact_data = { + "id": 1, + "name": "first", + "email": "first@example.com", + } + responses.add( + responses.POST, + api_url + "/contacts/", + content_type='application/json', + status=201, # This HTTP status still indicates success, + # even though it's not 200. + body=json.dumps(contact_data), + ) + contact = api_client.contacts.create() + contact.save() + assert len(responses.calls) == 1 +>>>>>>> Refactor how client validates responses From 0ae62ce1515a636ac82d03e2d6f10f51a734243d Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 15:57:00 -0400 Subject: [PATCH 112/451] Add test to verify handling redirects --- tests/test_client.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index d73ea2e9..53669e7f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -184,20 +184,16 @@ def test_call_resource_method(mocked_responses, api_client, api_url): Contact, 1, "remove_duplicates", {} ) assert isinstance(contact, Contact) -<<<<<<< HEAD assert len(mocked_responses.calls) == 1 -======= - assert len(responses.calls) == 1 -@responses.activate -def test_201_response(api_client, api_url): +def test_201_response(mocked_responses, api_client, api_url): contact_data = { "id": 1, "name": "first", "email": "first@example.com", } - responses.add( + mocked_responses.add( responses.POST, api_url + "/contacts/", content_type='application/json', @@ -207,5 +203,29 @@ def test_201_response(api_client, api_url): ) contact = api_client.contacts.create() contact.save() - assert len(responses.calls) == 1 ->>>>>>> Refactor how client validates responses + assert len(mocked_responses.calls) == 1 + + +def test_301_response(mocked_responses, api_client, api_url): + contact_data = { + "id": 1, + "name": "first", + "email": "first@example.com", + } + mocked_responses.add( + responses.GET, + api_url + "/contacts/first", + status=301, + headers={"Location": api_url + "/contacts/1"} + ) + mocked_responses.add( + responses.GET, + api_url + "/contacts/1", + content_type='application/json', + status=200, + body=json.dumps(contact_data), + ) + contact = api_client.contacts.find("first") + assert contact["id"] == 1 + assert contact["name"] == "first" + assert len(mocked_responses.calls) == 2 From 07cae4251690ea3abdc4016bd97b79e9b6993c2b Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 15:59:52 -0400 Subject: [PATCH 113/451] py2 compat --- nylas/client/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index adf53414..4674bcd1 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -14,6 +14,10 @@ Label, Draft ) from nylas.client.errors import APIClientError, ConnectionError, STATUS_MAP +try: + from json import JSONDecodeError +except ImportError: + JSONDecodeError = ValueError DEBUG = environ.get('NYLAS_CLIENT_DEBUG') API_SERVER = "https://api.nylas.com" @@ -40,7 +44,7 @@ def _validate(response): try: data = response.json() json_content = True - except json.JSONDecodeError: + except JSONDecodeError: data = response.content json_content = False From c0a8d1f54e4e0f9894637fad249234c5626dcbbd Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Thu, 3 Aug 2017 15:19:36 -0400 Subject: [PATCH 114/451] Ensure draft versions are updated --- tests/conftest.py | 2 +- tests/test_drafts.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index af96ba90..0974ec1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -574,6 +574,7 @@ def request_callback(request): } 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( @@ -638,7 +639,6 @@ def mock_draft_sent_response(mocked_responses, api_url): def callback(request): payload = json.loads(request.body) assert payload['draft_id'] == '2h111aefv8pzwzfykrn7hercj' - assert payload['version'] == 0 return values.pop() mocked_responses.add_callback( diff --git a/tests/test_drafts.py b/tests/test_drafts.py index f8747c3a..6a92a18a 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -62,3 +62,15 @@ def test_delete_draft(api_client): draft.save() # ... and delete it for real. draft.delete() + + +@pytest.mark.usefixtures("mock_draft_saved_response") +def test_draft_version(api_client): + draft = api_client.drafts.create() + assert 'version' not in draft + draft.save() + assert draft['version'] == 1 + draft.save() + assert draft['version'] == 2 + draft.save() + assert draft['version'] == 3 From 800c65a50434211be549cc0562aa642c5dafa9df Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 11:10:07 -0400 Subject: [PATCH 115/451] Version starts at 0 --- tests/conftest.py | 11 +++++++---- tests/test_drafts.py | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0974ec1e..68c3b132 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -560,10 +560,13 @@ def mock_draft_saved_response(mocked_responses, api_url): } ], "unread": False, - "version": 0 + "version": 0, } - def request_callback(request): + def create_callback(request): + return (200, {}, json.dumps(draft_json)) + + def update_callback(request): try: payload = json.loads(request.body) except ValueError: @@ -581,14 +584,14 @@ def request_callback(request): responses.POST, api_url + '/drafts/', content_type='application/json', - callback=request_callback, + callback=create_callback, ) mocked_responses.add_callback( responses.PUT, api_url + '/drafts/2h111aefv8pzwzfykrn7hercj', content_type='application/json', - callback=request_callback, + callback=update_callback, ) diff --git a/tests/test_drafts.py b/tests/test_drafts.py index 6a92a18a..bfd84d7f 100644 --- a/tests/test_drafts.py +++ b/tests/test_drafts.py @@ -69,8 +69,8 @@ def test_draft_version(api_client): draft = api_client.drafts.create() assert 'version' not in draft draft.save() + assert draft['version'] == 0 + draft.update() assert draft['version'] == 1 - draft.save() + draft.update() assert draft['version'] == 2 - draft.save() - assert draft['version'] == 3 From d33f686cca1c635fa6d4bf4edcf8afbc69ae2a55 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 11:13:46 -0400 Subject: [PATCH 116/451] pylint --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 68c3b132..e8b3efc1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -563,7 +563,7 @@ def mock_draft_saved_response(mocked_responses, api_url): "version": 0, } - def create_callback(request): + def create_callback(_request): return (200, {}, json.dumps(draft_json)) def update_callback(request): From a22415b639c827ecd62b5b7149318bfe78f934f9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 14:12:28 -0400 Subject: [PATCH 117/451] Remove old examples --- .../examples-new}/hosted-oauth/README.md | 0 .../examples-new}/hosted-oauth/config.json | 0 .../hosted-oauth/requirements.txt | 0 .../examples-new}/hosted-oauth/server.py | 0 .../templates/after_authorized.html | 0 .../hosted-oauth/templates/base.html | 0 .../templates/before_authorized.html | 0 .../native-authentication-exchange/README.md | 0 .../config.json | 0 .../requirements.txt | 0 .../native-authentication-exchange/server.py | 0 .../templates/base.html | 0 .../templates/index.html | 0 .../templates/missing_token.html | 0 .../templates/success.html | 0 .../native-authentication-gmail/README.md | 0 .../native-authentication-gmail/config.json | 0 .../requirements.txt | 0 .../native-authentication-gmail/server.py | 0 .../templates/after_connected.html | 0 .../templates/after_google.html | 0 .../templates/base.html | 0 .../templates/before_google.html | 0 .../examples-new}/webhooks/README.md | 0 .../examples-new}/webhooks/config.json | 0 .../examples-new}/webhooks/requirements.txt | 0 .../examples-new}/webhooks/server.py | 0 .../webhooks/templates/base.html | 0 .../webhooks/templates/index.html | 0 examples/lib/__init__.py | 0 examples/lib/random_words.py | 72 ------- examples/most_talked_to.py | 21 -- examples/nylas-connect/.gitignore | 3 - examples/nylas-connect/app.py | 194 ------------------ .../nylas-connect/credentials.py.template | 4 - examples/nylas-connect/readme.md | 103 ---------- examples/nylas-connect/requirements.txt | 18 -- examples/nylas-connect/templates/index.html | 21 -- examples/server.py | 104 ---------- examples/upload_and_download.py | 36 ---- examples/upload_files_in_dir.py | 34 --- examples/webhooks/.gitignore | 3 - examples/webhooks/app.py | 78 ------- examples/webhooks/credentials.py.template | 1 - examples/webhooks/readme.md | 49 ----- examples/webhooks/requirements.txt | 7 - 46 files changed, 748 deletions(-) rename {examples-new => examples/examples-new}/hosted-oauth/README.md (100%) rename {examples-new => examples/examples-new}/hosted-oauth/config.json (100%) rename {examples-new => examples/examples-new}/hosted-oauth/requirements.txt (100%) rename {examples-new => examples/examples-new}/hosted-oauth/server.py (100%) rename {examples-new => examples/examples-new}/hosted-oauth/templates/after_authorized.html (100%) rename {examples-new => examples/examples-new}/hosted-oauth/templates/base.html (100%) rename {examples-new => examples/examples-new}/hosted-oauth/templates/before_authorized.html (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/README.md (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/config.json (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/requirements.txt (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/server.py (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/templates/base.html (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/templates/index.html (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/templates/missing_token.html (100%) rename {examples-new => examples/examples-new}/native-authentication-exchange/templates/success.html (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/README.md (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/config.json (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/requirements.txt (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/server.py (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/templates/after_connected.html (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/templates/after_google.html (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/templates/base.html (100%) rename {examples-new => examples/examples-new}/native-authentication-gmail/templates/before_google.html (100%) rename {examples-new => examples/examples-new}/webhooks/README.md (100%) rename {examples-new => examples/examples-new}/webhooks/config.json (100%) rename {examples-new => examples/examples-new}/webhooks/requirements.txt (100%) rename {examples-new => examples/examples-new}/webhooks/server.py (100%) rename {examples-new => examples/examples-new}/webhooks/templates/base.html (100%) rename {examples-new => examples/examples-new}/webhooks/templates/index.html (100%) delete mode 100644 examples/lib/__init__.py delete mode 100755 examples/lib/random_words.py delete mode 100644 examples/most_talked_to.py delete mode 100644 examples/nylas-connect/.gitignore delete mode 100755 examples/nylas-connect/app.py delete mode 100644 examples/nylas-connect/credentials.py.template delete mode 100644 examples/nylas-connect/readme.md delete mode 100644 examples/nylas-connect/requirements.txt delete mode 100644 examples/nylas-connect/templates/index.html delete mode 100755 examples/server.py delete mode 100755 examples/upload_and_download.py delete mode 100755 examples/upload_files_in_dir.py delete mode 100644 examples/webhooks/.gitignore delete mode 100755 examples/webhooks/app.py delete mode 100644 examples/webhooks/credentials.py.template delete mode 100644 examples/webhooks/readme.md delete mode 100644 examples/webhooks/requirements.txt diff --git a/examples-new/hosted-oauth/README.md b/examples/examples-new/hosted-oauth/README.md similarity index 100% rename from examples-new/hosted-oauth/README.md rename to examples/examples-new/hosted-oauth/README.md diff --git a/examples-new/hosted-oauth/config.json b/examples/examples-new/hosted-oauth/config.json similarity index 100% rename from examples-new/hosted-oauth/config.json rename to examples/examples-new/hosted-oauth/config.json diff --git a/examples-new/hosted-oauth/requirements.txt b/examples/examples-new/hosted-oauth/requirements.txt similarity index 100% rename from examples-new/hosted-oauth/requirements.txt rename to examples/examples-new/hosted-oauth/requirements.txt diff --git a/examples-new/hosted-oauth/server.py b/examples/examples-new/hosted-oauth/server.py similarity index 100% rename from examples-new/hosted-oauth/server.py rename to examples/examples-new/hosted-oauth/server.py diff --git a/examples-new/hosted-oauth/templates/after_authorized.html b/examples/examples-new/hosted-oauth/templates/after_authorized.html similarity index 100% rename from examples-new/hosted-oauth/templates/after_authorized.html rename to examples/examples-new/hosted-oauth/templates/after_authorized.html diff --git a/examples-new/hosted-oauth/templates/base.html b/examples/examples-new/hosted-oauth/templates/base.html similarity index 100% rename from examples-new/hosted-oauth/templates/base.html rename to examples/examples-new/hosted-oauth/templates/base.html diff --git a/examples-new/hosted-oauth/templates/before_authorized.html b/examples/examples-new/hosted-oauth/templates/before_authorized.html similarity index 100% rename from examples-new/hosted-oauth/templates/before_authorized.html rename to examples/examples-new/hosted-oauth/templates/before_authorized.html diff --git a/examples-new/native-authentication-exchange/README.md b/examples/examples-new/native-authentication-exchange/README.md similarity index 100% rename from examples-new/native-authentication-exchange/README.md rename to examples/examples-new/native-authentication-exchange/README.md diff --git a/examples-new/native-authentication-exchange/config.json b/examples/examples-new/native-authentication-exchange/config.json similarity index 100% rename from examples-new/native-authentication-exchange/config.json rename to examples/examples-new/native-authentication-exchange/config.json diff --git a/examples-new/native-authentication-exchange/requirements.txt b/examples/examples-new/native-authentication-exchange/requirements.txt similarity index 100% rename from examples-new/native-authentication-exchange/requirements.txt rename to examples/examples-new/native-authentication-exchange/requirements.txt diff --git a/examples-new/native-authentication-exchange/server.py b/examples/examples-new/native-authentication-exchange/server.py similarity index 100% rename from examples-new/native-authentication-exchange/server.py rename to examples/examples-new/native-authentication-exchange/server.py diff --git a/examples-new/native-authentication-exchange/templates/base.html b/examples/examples-new/native-authentication-exchange/templates/base.html similarity index 100% rename from examples-new/native-authentication-exchange/templates/base.html rename to examples/examples-new/native-authentication-exchange/templates/base.html diff --git a/examples-new/native-authentication-exchange/templates/index.html b/examples/examples-new/native-authentication-exchange/templates/index.html similarity index 100% rename from examples-new/native-authentication-exchange/templates/index.html rename to examples/examples-new/native-authentication-exchange/templates/index.html diff --git a/examples-new/native-authentication-exchange/templates/missing_token.html b/examples/examples-new/native-authentication-exchange/templates/missing_token.html similarity index 100% rename from examples-new/native-authentication-exchange/templates/missing_token.html rename to examples/examples-new/native-authentication-exchange/templates/missing_token.html diff --git a/examples-new/native-authentication-exchange/templates/success.html b/examples/examples-new/native-authentication-exchange/templates/success.html similarity index 100% rename from examples-new/native-authentication-exchange/templates/success.html rename to examples/examples-new/native-authentication-exchange/templates/success.html diff --git a/examples-new/native-authentication-gmail/README.md b/examples/examples-new/native-authentication-gmail/README.md similarity index 100% rename from examples-new/native-authentication-gmail/README.md rename to examples/examples-new/native-authentication-gmail/README.md diff --git a/examples-new/native-authentication-gmail/config.json b/examples/examples-new/native-authentication-gmail/config.json similarity index 100% rename from examples-new/native-authentication-gmail/config.json rename to examples/examples-new/native-authentication-gmail/config.json diff --git a/examples-new/native-authentication-gmail/requirements.txt b/examples/examples-new/native-authentication-gmail/requirements.txt similarity index 100% rename from examples-new/native-authentication-gmail/requirements.txt rename to examples/examples-new/native-authentication-gmail/requirements.txt diff --git a/examples-new/native-authentication-gmail/server.py b/examples/examples-new/native-authentication-gmail/server.py similarity index 100% rename from examples-new/native-authentication-gmail/server.py rename to examples/examples-new/native-authentication-gmail/server.py diff --git a/examples-new/native-authentication-gmail/templates/after_connected.html b/examples/examples-new/native-authentication-gmail/templates/after_connected.html similarity index 100% rename from examples-new/native-authentication-gmail/templates/after_connected.html rename to examples/examples-new/native-authentication-gmail/templates/after_connected.html diff --git a/examples-new/native-authentication-gmail/templates/after_google.html b/examples/examples-new/native-authentication-gmail/templates/after_google.html similarity index 100% rename from examples-new/native-authentication-gmail/templates/after_google.html rename to examples/examples-new/native-authentication-gmail/templates/after_google.html diff --git a/examples-new/native-authentication-gmail/templates/base.html b/examples/examples-new/native-authentication-gmail/templates/base.html similarity index 100% rename from examples-new/native-authentication-gmail/templates/base.html rename to examples/examples-new/native-authentication-gmail/templates/base.html diff --git a/examples-new/native-authentication-gmail/templates/before_google.html b/examples/examples-new/native-authentication-gmail/templates/before_google.html similarity index 100% rename from examples-new/native-authentication-gmail/templates/before_google.html rename to examples/examples-new/native-authentication-gmail/templates/before_google.html diff --git a/examples-new/webhooks/README.md b/examples/examples-new/webhooks/README.md similarity index 100% rename from examples-new/webhooks/README.md rename to examples/examples-new/webhooks/README.md diff --git a/examples-new/webhooks/config.json b/examples/examples-new/webhooks/config.json similarity index 100% rename from examples-new/webhooks/config.json rename to examples/examples-new/webhooks/config.json diff --git a/examples-new/webhooks/requirements.txt b/examples/examples-new/webhooks/requirements.txt similarity index 100% rename from examples-new/webhooks/requirements.txt rename to examples/examples-new/webhooks/requirements.txt diff --git a/examples-new/webhooks/server.py b/examples/examples-new/webhooks/server.py similarity index 100% rename from examples-new/webhooks/server.py rename to examples/examples-new/webhooks/server.py diff --git a/examples-new/webhooks/templates/base.html b/examples/examples-new/webhooks/templates/base.html similarity index 100% rename from examples-new/webhooks/templates/base.html rename to examples/examples-new/webhooks/templates/base.html diff --git a/examples-new/webhooks/templates/index.html b/examples/examples-new/webhooks/templates/index.html similarity index 100% rename from examples-new/webhooks/templates/index.html rename to examples/examples-new/webhooks/templates/index.html diff --git a/examples/lib/__init__.py b/examples/lib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/lib/random_words.py b/examples/lib/random_words.py deleted file mode 100755 index 07174dda..00000000 --- a/examples/lib/random_words.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -import random -import json -import sys - -DICT_FILE = '/etc/dictionaries-common/words' - -def get_words(): - words = [] - try: - with open(DICT_FILE, 'r') as f: - words.extend(f.read().split('\n')) - except IOError: - print(json.dumps({'error': "couldn't open dictionary file", - 'filename': DICT_FILE})) - sys.exit(1) - return words - - -def random_words(count=int(random.uniform(1,500)), sig='me'): - words = get_words() - random_word_list = [] - - if sig: - word_index = int(random.uniform(1, len(words))) - random_word = words[word_index] - - salutation = ['Hey', 'Hi', 'Ahoy', 'Yo'][int(random.uniform(0,3))] - random_word_list.append("{} {},\n\n".format(salutation, random_word)) - - - just_entered = False - for i in range(count): - word_index = int(random.uniform(1, len(words))) - random_word = words[word_index] - - if i > 0 and not just_entered: - random_word = ' ' + random_word - - just_entered = False - - if int(random.uniform(1,15)) == 1: - random_word += ('.') - - if int(random.uniform(1,3)) == 1 and sig: - random_word += ('\n') - just_entered = True - - if int(random.uniform(1,3)) == 1 and sig: - random_word += ('\n') - just_entered = True - - random_word_list.append(random_word) - - text = ''.join(random_word_list) + '.' - if sig: - if int(random.uniform(1,2)) == 1: - salutation = ['Cheers', 'Adios', 'Ciao', 'Bye'][int(random.uniform(0,3))] - punct = ['.', ',', '!', ''][int(random.uniform(0,3))] - text += "\n\n{}{}\n".format(salutation, punct) - else: - text += '\n\n' - - punct = ['-', '- ', '--', '-- '][int(random.uniform(0,3))] - text += '{}{}'.format(punct, sig) - - return text - - -if __name__ == '__main__': - print(random_words()) diff --git a/examples/most_talked_to.py b/examples/most_talked_to.py deleted file mode 100644 index 378e86e6..00000000 --- a/examples/most_talked_to.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/python -from __future__ import print_function -from operator import itemgetter -from nylas import APIClient - -APP_ID = '[YOUR_APP_ID]' -APP_SECRET = '[YOUR_APP_SECRET]' -ACCESS_TOKEN = '[YOUR_ACCESS_TOKEN]' -client = APIClient(APP_ID, APP_SECRET, ACCESS_TOKEN) - -counts = {} - -for m in client.messages: - for p in map(lambda x: x['email'], m['from']): - if p not in counts: - counts[p] = 0 - counts[p] += 1 - -most_chatted = sorted(counts.iteritems(), key=itemgetter(1)) -for i in most_chatted: - print(i) diff --git a/examples/nylas-connect/.gitignore b/examples/nylas-connect/.gitignore deleted file mode 100644 index cb272b17..00000000 --- a/examples/nylas-connect/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -credentials.py -env/ -*.pyc diff --git a/examples/nylas-connect/app.py b/examples/nylas-connect/app.py deleted file mode 100755 index 12c91cf3..00000000 --- a/examples/nylas-connect/app.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -import json -import sys -import os -import flask -import requests -import urllib -import logging -import subprocess -import uuid - -sys.path.append('../../') - -from nylas import APIClient - -# Sets the logging format -logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s') -log = logging.getLogger(__name__) -log.setLevel(logging.INFO) - -try: - from credentials import * -except ImportError: - log.error("Couldn't import credentials.py --- you'll need to create it.") - log.error("See credentials.py.template for more details.") - sys.exit(-1) - -app = flask.Flask(__name__) - -# These are the permissions your app will ask the user to approve for access -# https://developers.google.com/identity/protocols/OAuth2WebServer#scope -GOOGLE_SCOPES = ' '.join(['https://mail.google.com/', - 'https://www.googleapis.com/auth/calendar', - 'https://www.googleapis.com/auth/userinfo.email', - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/calendar', - 'https://www.google.com/m8/feeds/']) - -# This is the path in your Google application that users are redirected to after they -# have authenticated with Google, and it must be authorized through Google's -# developer console -REDIRECT_URI = '' # Note: Use your ngrok url here if testing locally -NYLAS_API = 'https://api.nylas.com' -OAUTH_TOKEN_VALIDATION_URL = 'https://www.googleapis.com/oauth2/v2/tokeninfo' - - -@app.route('/') -def index(): - if 'google_credentials' not in flask.session: - return flask.render_template('index.html') - - # The user has authorized with google at this point but we will need to - # connect the account to Nylas - if 'nylas_access_token' not in flask.session: - google_credentials = flask.session['google_credentials'] - google_access_token = google_credentials['access_token'] - email_address = get_email(google_access_token) - google_refresh_token = google_credentials['refresh_token'] - connect_to_nylas(google_refresh_token, email_address) - return flask.redirect(flask.url_for('index')) - - # Google account has been setup, let's use Nylas' python SDK to retrieve an - # email - client = APIClient(NYLAS_CLIENT_ID, NYLAS_CLIENT_SECRET, - flask.session['nylas_access_token']) - - # Display the latest email message! - return client.threads.first().messages.first().body - - -@app.route('/revoke') -def revoke_token(): - client = APIClient(NYLAS_CLIENT_ID, NYLAS_CLIENT_SECRET, flask.session['nylas_access_token']) - client.revoke_token() - - del flask.session['google_credentials'] - del flask.session['nylas_access_token'] - - return "Token revoked" if client.access_token is None else "Failed token revocation" - - -# This is the url Google will call once a user has approved access to their -# account -@app.route('/oauth2callback') -def oauth2callback(): - if 'code' not in flask.request.args: - params = {'response_type': 'code', - 'access_type': 'offline', - 'client_id': GOOGLE_CLIENT_ID, - 'redirect_uri': REDIRECT_URI, - 'scope': GOOGLE_SCOPES, - # Note: this is only for testing to ensure a refresh token is - # passed everytime, but requires the user to approve offline - # access every time. You should remove this if you don't want - # your user to have to approve access each time they connect - 'prompt': 'consent', - } - url_params = urllib.urlencode(params) - auth_uri = 'https://accounts.google.com/o/oauth2/v2/auth?{}'.format(url_params) - return flask.redirect(auth_uri) - else: - auth_code = flask.request.args.get('code') - data = {'code': auth_code, - 'client_id': GOOGLE_CLIENT_ID, - 'client_secret': GOOGLE_CLIENT_SECRET, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'authorization_code'} - r = requests.post('https://www.googleapis.com/oauth2/v4/token', data=data) - # This refresh token will only be returned once unless you prompt the user - # for consent every time, so be sure to remember it! - flask.session['google_credentials'] = r.json() - return flask.redirect(flask.url_for('index')) - - -# Connecting an account is a two step process once you have a refresh token from -# Google. -# First POST to /connect/authorize to get an authorization code from Nylas -# Then post to /connect/token to get an access_token that can be used to access -# account data -def connect_to_nylas(google_refresh_token, email_address): - google_settings = {'google_client_id': GOOGLE_CLIENT_ID, - 'google_client_secret': GOOGLE_CLIENT_SECRET, - 'google_refresh_token': google_refresh_token - } - - data = {'client_id': NYLAS_CLIENT_ID, - 'name': 'Your Name', - 'email_address': email_address, - 'provider': 'gmail', - 'settings': google_settings - } - code = nylas_code(data) - - data = {'client_id': NYLAS_CLIENT_ID, - 'client_secret': NYLAS_CLIENT_SECRET, - 'code': code - } - nylas_access_token = nylas_token(data) - - flask.session['nylas_access_token'] = nylas_access_token - - -# Uses googles tokeninfo endpoint to get an email address from the google -# access_token -def get_email(google_access_token): - r = requests.get(OAUTH_TOKEN_VALIDATION_URL, - params={'access_token': google_access_token, - 'fields': 'email'}) # specify we only want the email - resp = r.json() - log.info(resp) - return resp['email'] - - -def nylas_code(data): - connect_uri = '{}/connect/authorize'.format(NYLAS_API) - resp = requests.post(connect_uri, json=data).json() - log.info(resp) - if 'code' in resp: - return resp['code'] - - raise Exception("Error getting auth code from Nylas", err=resp) - - -def nylas_token(data): - token_uri = '{}/connect/token'.format(NYLAS_API) - resp = requests.post(token_uri, json=data).json() - log.info(resp) - if 'access_token' in resp: - return resp['access_token'] - - raise Exception("Error getting access token from Nylas", err=resp) - - -# Setup google developer settings to ensure everything works locally -def initialize(): - global REDIRECT_URI - REDIRECT_URI = "{}/oauth2callback".format("http://lvh.me:1234") - print(REDIRECT_URI) - s = raw_input("Have you added the url above as an authorized callback " - "in Google's Developer console? y/n ") - if s != "y": - print("You need to set that up first!") - print("See https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide for more information") - sys.exit(-1) - - -if __name__ == '__main__': - logging.info("Initializing Application") - initialize() - app.secret_key = str(uuid.uuid4()) - app.debug = False - print("Visit http://localhost:1234 in your browser") - app.run(port=1234) diff --git a/examples/nylas-connect/credentials.py.template b/examples/nylas-connect/credentials.py.template deleted file mode 100644 index 6fb22262..00000000 --- a/examples/nylas-connect/credentials.py.template +++ /dev/null @@ -1,4 +0,0 @@ -GOOGLE_CLIENT_ID = '' -GOOGLE_CLIENT_SECRET = '' -NYLAS_CLIENT_ID = '' -NYLAS_CLIENT_SECRET = '' diff --git a/examples/nylas-connect/readme.md b/examples/nylas-connect/readme.md deleted file mode 100644 index 5fcadac6..00000000 --- a/examples/nylas-connect/readme.md +++ /dev/null @@ -1,103 +0,0 @@ -# Nylas Connect - -This tiny flask app is a simple example of how to use Nylas' [Native -authentication APIs](https://www.nylas.com/docs/platform#native_authentication). -It shows how to receive a `refresh_token` from Google before authenticating with -Nylas. Then it uses the Nylas Python SDK to connect an email account and -load the user's latest email. - -While this steps through the Google OAuth flow manually, you can alternatively -use Google API SDKs for Python and many other languages. Learn more about that -[here](https://developers.google.com/api-client-library/python/). You can also -learn more about Google OAuth -[here](https://developers.google.com/identity/protocols/OAuth2WebServer). - -Here is an overview of the complete flow: - -``` -Your App Google -+------+ +-----+ -| | Redirect user to Oauth Login | | -| +----------------------------------> | | -| | | | -| | Authorization code | | -| | <--(localhost)-<-(ngrok)-----------+ | -| | | | -| | Request refresh token | | -| +----------------------------------> | | -| | | | -| | Refresh & access token | | -| | <----------------------------------+ | -| | +-----+ -| | -| | -| | Nylas -| | Request Authorization code +-----+ -| +----------------------------------> | | -| | | | -| | Authorization code | | -| | <----------------------------------+ | -| | | | -| | Request Access Token | | -| +----------------------------------> | | -| | | | -| | Access Token | | -| | <----------------------------------+ | -+------+ +-----+ -``` - - -# Getting Started - -## Dependencies - -### Google Application - -You'll need to have a Nylas [developer account](https://developer.nylas.com), a -Google Application, and the respective `client_id` and `client_secret`s. -Learn about how to setup the Google App to correctly work with Nylas -[here](https://support.nylas.com/hc/en-us/articles/222176307-Google-OAuth-Setup-Guide). - -### ngrok - -[ngrok](https://ngrok.com/) makes it really easy to test callback urls that are -running locally on your computer. - -### virtualenv - -Make sure `virtualenv` is installed. To install it type the following: - -```bash -pip install virtualenv -``` - -## Initial Setup - -Add your google and nylas client id's and secrets to a new file credentials.py. -See `credentials.py.template` for an example - - -Create a virtual env, activate it, and then install python dependencies - -```bash -virtualenv env -source env/bin/activate -pip install -r requirements.txt -``` - -# Running the app - -First, make sure ngrok is running with the same port that the local flask app is -running. - -```bash -ngrok http 1234 -``` - -Next, run the flask app. - -```bash -./app.py -``` - -Visit http://localhost:1234 in your browser. diff --git a/examples/nylas-connect/requirements.txt b/examples/nylas-connect/requirements.txt deleted file mode 100644 index d30bdc7f..00000000 --- a/examples/nylas-connect/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -bumpversion==0.5.3 -cffi==1.7.0 -click==6.6 -cryptography==1.4 -enum34==1.1.6 -Flask==0.11.1 -idna==2.1 -ipaddress==1.0.16 -itsdangerous==0.24 -Jinja2==2.8 -MarkupSafe==0.23 -ndg-httpsclient==0.4.2 -pyasn1==0.1.9 -pycparser==2.14 -pyOpenSSL==16.0.0 -requests==2.11.0 -six==1.10.0 -Werkzeug==0.11.10 diff --git a/examples/nylas-connect/templates/index.html b/examples/nylas-connect/templates/index.html deleted file mode 100644 index 1b17154c..00000000 --- a/examples/nylas-connect/templates/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - Nylas Connect - - - - - - - - -

Nylas Connect would like to authorize your email!

- Click to continue - - - diff --git a/examples/server.py b/examples/server.py deleted file mode 100755 index 2781c921..00000000 --- a/examples/server.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python -# -# This demo app shows how to use the Nylas client to authenticate against -# the Nylas API and how to fetch emails from an authenticated account. -# -# NOTE: This app does NOT use SSL. Before deploying this code to a -# server environment, you should ENABLE SSL to avoid exposing your API -# access token in plaintext. -# -# To run this demo app: -# 1. Save this file to your computer as `server.py` -# -# 2. In the Nylas Developer Portal, create a new application. Replace the -# APP_ID and APP_SECRET variables below with the App ID and App -# Secret of your application. -# https://nylas.com/ -# -# 3. In the Nylas Developer Portal, edit your application and add the -# callback URL: http://localhost:8888/login_callback -# -# 4. On the command line, `cd` to the folder where you saved the file -# -# 5. On the command line, run `python ./server.py` -# - You may need to install Python: https://www.python.org/download/ -# - You may need to install dependencies using pip: -# (http://pip.readthedocs.org/en/latest/installing.html) -# pip install nylas flask requests -# - Note: You may want to set up a virtualenv to isolate these -# dependencies from other packages on your system. Otherwise, you -# will need to sudo pip install, to install them globally. -# http://docs.python-guide.org/en/latest/dev/virtualenvs/ -# -# 6. In the browser, visit http://localhost:8888/ -# - -from __future__ import print_function - -import time -from flask import Flask, url_for, session, request, redirect, Response - -from nylas import APIClient - -APP_ID = 'YOUR_APP_ID' -APP_SECRET = 'YOUR_APP_SECRET' - -app = Flask(__name__) -app.debug = True -app.secret_key = 'secret' - -assert APP_ID != 'YOUR_APP_ID' or APP_SECRET != 'YOUR_APP_SECRET',\ - "You should change the value of APP_ID and APP_SECRET" - - -@app.route('/') -def index(): - # If we have an access_token, we may interact with the Nylas Server - if 'access_token' in session: - client = APIClient(APP_ID, APP_SECRET, session['access_token']) - message = None - while not message: - try: - # Get the latest message from namespace zero. - message = client.messages.first() - if not message: # A new account takes a little time to sync - print("No messages yet. Checking again in 2 seconds.") - time.sleep(2) - except Exception as e: - print(e.message) - return Response("An error occurred.") - # Format the output - text = "

Here's a message from your inbox:

From: " - for sender in message["from"]: - text += "{} <{}>".format(sender['name'], sender['email']) - text += "
Subject: " + message.subject - text += "
Body: " + message.body - text += "" - - # Return result to the client - return Response(text) - else: - # We don't have an access token, so we're going to use OAuth to - # authenticate the user - - # Ask flask to generate the url corresponding to the login_callback - # route. This is similar to using reverse() in django. - redirect_uri = url_for('.login_callback', _external=True) - - client = APIClient(APP_ID, APP_SECRET) - return redirect(client.authentication_url(redirect_uri)) - - -@app.route('/login_callback') -def login_callback(): - if 'error' in request.args: - return "Login error: {0}".format(request.args['error']) - - # Exchange the authorization code for an access token - client = APIClient(APP_ID, APP_SECRET) - code = request.args.get('code') - session['access_token'] = client.token_for_code(code) - return index() - -if __name__ == '__main__': - app.run(host='0.0.0.0', port=8888) diff --git a/examples/upload_and_download.py b/examples/upload_and_download.py deleted file mode 100755 index 3e98a0d8..00000000 --- a/examples/upload_and_download.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python -from __future__ import print_function -import time -from nylas import APIClient - -APP_ID = '[YOUR_APP_ID]' -APP_SECRET = '[YOUR_APP_SECRET]' -ACCESS_TOKEN = '[YOUR_ACCESS_TOKEN]' -client = APIClient(APP_ID, APP_SECRET, ACCESS_TOKEN) - -f = open('test.py', 'r') -data = f.read() -f.close() - -myfile = client.files.create() -myfile.filename = 'test.py' -myfile.data = data - -# Create a new draft -draft = client.drafts.create() -draft.to = [{'name': 'Charles Gruenwald', 'email': 'nylastestempty@gmail.com'}] -draft.subject = 'nylas test' -draft.body = "" -draft.attach(myfile) -draft.send() - -x = 0 -th = client.threads.where({'in': 'Sent', 'subject': subject}).first() -while not th: - time.sleep(0.5) - x += 1 - th = client.threads.where({'in': 'Sent', 'subject': subject}).first() - -m = th.messages[0] - -print(m.attachments[0].download()) diff --git a/examples/upload_files_in_dir.py b/examples/upload_files_in_dir.py deleted file mode 100755 index 0a8b4858..00000000 --- a/examples/upload_files_in_dir.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/python -from __future__ import print_function -import os -import time -from nylas import APIClient - -APP_ID = '[YOUR_APP_ID]' -APP_SECRET = '[YOUR_APP_SECRET]' -ACCESS_TOKEN = '[YOUR_ACCESS_TOKEN]' -client = APIClient(APP_ID, APP_SECRET, ACCESS_TOKEN) - -subject = 'nylas test' -# Create a new draft -draft = client.drafts.create() -draft.to = [{'name': 'Nylas PythonSDK', 'email': 'nylastestempty@gmail.com'}] -draft.subject = subject -draft.body = "" - -for filename in filter(lambda x: not os.path.isdir(x), os.listdir(".")): - f = open(filename, 'r') - attachment = client.files.create() - attachment.filename = filename - attachment.stream = f - attachment.save() - draft.attach(attachment) - -draft.send() - -th = client.threads.where({'in': 'Sent', 'subject': subject}).first() -while not th: - time.sleep(0.5) - th = client.threads.where({'in': 'Sent', 'subject': subject}).first() - -print(th.messages[0].attachments[0].download()) diff --git a/examples/webhooks/.gitignore b/examples/webhooks/.gitignore deleted file mode 100644 index cb272b17..00000000 --- a/examples/webhooks/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -credentials.py -env/ -*.pyc diff --git a/examples/webhooks/app.py b/examples/webhooks/app.py deleted file mode 100755 index e0bdb302..00000000 --- a/examples/webhooks/app.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function -import sys -import json -import flask -import requests -import hmac -import hashlib -import uuid - -try: - from credentials import * -except ImportError: - log.error("Couldn't import credentials.py --- you'll need to create it.") - log.error("See credentials.py.template for more details.") - sys.exit(-1) - -app = flask.Flask(__name__) - - -@app.route('/webhook', methods=['GET', 'POST']) -def index(): - # Nylas will check to make sure your webhook is valid by making a GET - # request to your endpoint with a challenge parameter when you add the - # endpoint to the developer dashboard. All you have to do is return the - # value of the challenge parameter in the body of the response. - if flask.request.method == 'GET' and 'challenge' in flask.request.args: - return flask.request.args['challenge'] - # Nylas sent us a webhook notification for some kind of event, so we should - # process it! - elif flask.request.method == 'POST': - # Verify the request to make sure it's actually from Nylas. - if not verify_request(flask.request): - return "X-Nylas-Signature failed verification", 401 - # Nylas will send us a json object of the deltas. - data = json.loads(flask.request.data) - for delta in data['deltas']: - # Print some of the information Nylas sent us. This is where you - # would normally process the webhook notification and do things like - # fetch relevant message ids, update your database, etc. - print("{} at {} with id {}".format(delta['type'], delta['date'], - delta['object_data']['id'])) - # Don't forget to let Nylas know that everything was pretty ok. - return "Success", 200 - - else: - # We only allow GET and POST requests to this endpoint. - return "Method not allowed", 405 - - -# Each request made by Nylas includes an X-Nylas-Signature header. The header -# contains the HMAC-SHA256 signature of the request body, using your client -# secret as the signing key. This allows your app to verify that the -# notification really came from Nylas. -def verify_request(request): - digest = hmac.new(NYLAS_CLIENT_SECRET, msg=request.data, digestmod=hashlib.sha256).hexdigest() - return digest == request.headers.get('X-Nylas-Signature') - - -# Setup ngrok settings to ensure everything works locally -def initialize(): - # Make sure ngrok is running - try: - resp = requests.get('http://localhost:4040/api/tunnels').json() - except requests.exceptions.ConnectionError: - print("It looks like ngrok isn't running! Make sure you've started that first with 'ngrok http 1234'") - sys.exit(-1) - - global WEBHOOK_URI - WEBHOOK_URI = "{}/webhook".format(resp['tunnels'][1]['public_url']) - - -if __name__ == '__main__': - initialize() - app.secret_key = str(uuid.uuid4()) - app.debug = False - print("{}\nAdd the above url to the webhooks page at https://developer.nylas.com".format(WEBHOOK_URI)) - app.run(port=1234) diff --git a/examples/webhooks/credentials.py.template b/examples/webhooks/credentials.py.template deleted file mode 100644 index f61e903b..00000000 --- a/examples/webhooks/credentials.py.template +++ /dev/null @@ -1 +0,0 @@ -NYLAS_CLIENT_SECRET = '' diff --git a/examples/webhooks/readme.md b/examples/webhooks/readme.md deleted file mode 100644 index 9a81e82c..00000000 --- a/examples/webhooks/readme.md +++ /dev/null @@ -1,49 +0,0 @@ -# Nylas Webhooks - -This tiny flask app is a simple example of how to use Nylas' webhooks feature. -This app correctly responds to Nylas' challenge request when you add a webhook -url to the [developer dashboard](https://developer.nylas.com). It also verifies -any webhook notification POST requests by Nylas and prints out some information -about the notification. - -# Dependencies - -## ngrok - -[ngrok](https://ngrok.com/) makes it really easy to test callback urls that are -running locally on your computer. - -## virtualenv - -Make sure `virtualenv` is installed. To install it type the following: - -```bash -pip install virtualenv -``` - -# Initial Setup - -Create a virtual env, activate it, and then install python dependencies - -```bash -virtualenv env -source env/bin/activate -pip install -r requirements.txt -``` - -# Running the app - -First, make sure ngrok is running with the same port that the local flask app is -running. - -```bash -ngrok http 1234 -``` - -Next, run the flask app. - -```bash -./app.py -``` - -Follow the instructions that are printed to the console. diff --git a/examples/webhooks/requirements.txt b/examples/webhooks/requirements.txt deleted file mode 100644 index ce725103..00000000 --- a/examples/webhooks/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -click==6.6 -Flask==0.11.1 -itsdangerous==0.24 -Jinja2==2.8 -MarkupSafe==0.23 -requests==2.11.1 -Werkzeug==0.11.10 From 2ace780e5c864ba4e4a74ba30e54f329526335a9 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 17:24:56 -0400 Subject: [PATCH 118/451] oops --- examples/{examples-new => }/hosted-oauth/README.md | 0 examples/{examples-new => }/hosted-oauth/config.json | 0 examples/{examples-new => }/hosted-oauth/requirements.txt | 0 examples/{examples-new => }/hosted-oauth/server.py | 0 .../hosted-oauth/templates/after_authorized.html | 0 examples/{examples-new => }/hosted-oauth/templates/base.html | 0 .../hosted-oauth/templates/before_authorized.html | 0 .../{examples-new => }/native-authentication-exchange/README.md | 0 .../{examples-new => }/native-authentication-exchange/config.json | 0 .../native-authentication-exchange/requirements.txt | 0 .../{examples-new => }/native-authentication-exchange/server.py | 0 .../native-authentication-exchange/templates/base.html | 0 .../native-authentication-exchange/templates/index.html | 0 .../native-authentication-exchange/templates/missing_token.html | 0 .../native-authentication-exchange/templates/success.html | 0 examples/{examples-new => }/native-authentication-gmail/README.md | 0 .../{examples-new => }/native-authentication-gmail/config.json | 0 .../native-authentication-gmail/requirements.txt | 0 examples/{examples-new => }/native-authentication-gmail/server.py | 0 .../native-authentication-gmail/templates/after_connected.html | 0 .../native-authentication-gmail/templates/after_google.html | 0 .../native-authentication-gmail/templates/base.html | 0 .../native-authentication-gmail/templates/before_google.html | 0 examples/{examples-new => }/webhooks/README.md | 0 examples/{examples-new => }/webhooks/config.json | 0 examples/{examples-new => }/webhooks/requirements.txt | 0 examples/{examples-new => }/webhooks/server.py | 0 examples/{examples-new => }/webhooks/templates/base.html | 0 examples/{examples-new => }/webhooks/templates/index.html | 0 29 files changed, 0 insertions(+), 0 deletions(-) rename examples/{examples-new => }/hosted-oauth/README.md (100%) rename examples/{examples-new => }/hosted-oauth/config.json (100%) rename examples/{examples-new => }/hosted-oauth/requirements.txt (100%) rename examples/{examples-new => }/hosted-oauth/server.py (100%) rename examples/{examples-new => }/hosted-oauth/templates/after_authorized.html (100%) rename examples/{examples-new => }/hosted-oauth/templates/base.html (100%) rename examples/{examples-new => }/hosted-oauth/templates/before_authorized.html (100%) rename examples/{examples-new => }/native-authentication-exchange/README.md (100%) rename examples/{examples-new => }/native-authentication-exchange/config.json (100%) rename examples/{examples-new => }/native-authentication-exchange/requirements.txt (100%) rename examples/{examples-new => }/native-authentication-exchange/server.py (100%) rename examples/{examples-new => }/native-authentication-exchange/templates/base.html (100%) rename examples/{examples-new => }/native-authentication-exchange/templates/index.html (100%) rename examples/{examples-new => }/native-authentication-exchange/templates/missing_token.html (100%) rename examples/{examples-new => }/native-authentication-exchange/templates/success.html (100%) rename examples/{examples-new => }/native-authentication-gmail/README.md (100%) rename examples/{examples-new => }/native-authentication-gmail/config.json (100%) rename examples/{examples-new => }/native-authentication-gmail/requirements.txt (100%) rename examples/{examples-new => }/native-authentication-gmail/server.py (100%) rename examples/{examples-new => }/native-authentication-gmail/templates/after_connected.html (100%) rename examples/{examples-new => }/native-authentication-gmail/templates/after_google.html (100%) rename examples/{examples-new => }/native-authentication-gmail/templates/base.html (100%) rename examples/{examples-new => }/native-authentication-gmail/templates/before_google.html (100%) rename examples/{examples-new => }/webhooks/README.md (100%) rename examples/{examples-new => }/webhooks/config.json (100%) rename examples/{examples-new => }/webhooks/requirements.txt (100%) rename examples/{examples-new => }/webhooks/server.py (100%) rename examples/{examples-new => }/webhooks/templates/base.html (100%) rename examples/{examples-new => }/webhooks/templates/index.html (100%) diff --git a/examples/examples-new/hosted-oauth/README.md b/examples/hosted-oauth/README.md similarity index 100% rename from examples/examples-new/hosted-oauth/README.md rename to examples/hosted-oauth/README.md diff --git a/examples/examples-new/hosted-oauth/config.json b/examples/hosted-oauth/config.json similarity index 100% rename from examples/examples-new/hosted-oauth/config.json rename to examples/hosted-oauth/config.json diff --git a/examples/examples-new/hosted-oauth/requirements.txt b/examples/hosted-oauth/requirements.txt similarity index 100% rename from examples/examples-new/hosted-oauth/requirements.txt rename to examples/hosted-oauth/requirements.txt diff --git a/examples/examples-new/hosted-oauth/server.py b/examples/hosted-oauth/server.py similarity index 100% rename from examples/examples-new/hosted-oauth/server.py rename to examples/hosted-oauth/server.py diff --git a/examples/examples-new/hosted-oauth/templates/after_authorized.html b/examples/hosted-oauth/templates/after_authorized.html similarity index 100% rename from examples/examples-new/hosted-oauth/templates/after_authorized.html rename to examples/hosted-oauth/templates/after_authorized.html diff --git a/examples/examples-new/hosted-oauth/templates/base.html b/examples/hosted-oauth/templates/base.html similarity index 100% rename from examples/examples-new/hosted-oauth/templates/base.html rename to examples/hosted-oauth/templates/base.html diff --git a/examples/examples-new/hosted-oauth/templates/before_authorized.html b/examples/hosted-oauth/templates/before_authorized.html similarity index 100% rename from examples/examples-new/hosted-oauth/templates/before_authorized.html rename to examples/hosted-oauth/templates/before_authorized.html diff --git a/examples/examples-new/native-authentication-exchange/README.md b/examples/native-authentication-exchange/README.md similarity index 100% rename from examples/examples-new/native-authentication-exchange/README.md rename to examples/native-authentication-exchange/README.md diff --git a/examples/examples-new/native-authentication-exchange/config.json b/examples/native-authentication-exchange/config.json similarity index 100% rename from examples/examples-new/native-authentication-exchange/config.json rename to examples/native-authentication-exchange/config.json diff --git a/examples/examples-new/native-authentication-exchange/requirements.txt b/examples/native-authentication-exchange/requirements.txt similarity index 100% rename from examples/examples-new/native-authentication-exchange/requirements.txt rename to examples/native-authentication-exchange/requirements.txt diff --git a/examples/examples-new/native-authentication-exchange/server.py b/examples/native-authentication-exchange/server.py similarity index 100% rename from examples/examples-new/native-authentication-exchange/server.py rename to examples/native-authentication-exchange/server.py diff --git a/examples/examples-new/native-authentication-exchange/templates/base.html b/examples/native-authentication-exchange/templates/base.html similarity index 100% rename from examples/examples-new/native-authentication-exchange/templates/base.html rename to examples/native-authentication-exchange/templates/base.html diff --git a/examples/examples-new/native-authentication-exchange/templates/index.html b/examples/native-authentication-exchange/templates/index.html similarity index 100% rename from examples/examples-new/native-authentication-exchange/templates/index.html rename to examples/native-authentication-exchange/templates/index.html diff --git a/examples/examples-new/native-authentication-exchange/templates/missing_token.html b/examples/native-authentication-exchange/templates/missing_token.html similarity index 100% rename from examples/examples-new/native-authentication-exchange/templates/missing_token.html rename to examples/native-authentication-exchange/templates/missing_token.html diff --git a/examples/examples-new/native-authentication-exchange/templates/success.html b/examples/native-authentication-exchange/templates/success.html similarity index 100% rename from examples/examples-new/native-authentication-exchange/templates/success.html rename to examples/native-authentication-exchange/templates/success.html diff --git a/examples/examples-new/native-authentication-gmail/README.md b/examples/native-authentication-gmail/README.md similarity index 100% rename from examples/examples-new/native-authentication-gmail/README.md rename to examples/native-authentication-gmail/README.md diff --git a/examples/examples-new/native-authentication-gmail/config.json b/examples/native-authentication-gmail/config.json similarity index 100% rename from examples/examples-new/native-authentication-gmail/config.json rename to examples/native-authentication-gmail/config.json diff --git a/examples/examples-new/native-authentication-gmail/requirements.txt b/examples/native-authentication-gmail/requirements.txt similarity index 100% rename from examples/examples-new/native-authentication-gmail/requirements.txt rename to examples/native-authentication-gmail/requirements.txt diff --git a/examples/examples-new/native-authentication-gmail/server.py b/examples/native-authentication-gmail/server.py similarity index 100% rename from examples/examples-new/native-authentication-gmail/server.py rename to examples/native-authentication-gmail/server.py diff --git a/examples/examples-new/native-authentication-gmail/templates/after_connected.html b/examples/native-authentication-gmail/templates/after_connected.html similarity index 100% rename from examples/examples-new/native-authentication-gmail/templates/after_connected.html rename to examples/native-authentication-gmail/templates/after_connected.html diff --git a/examples/examples-new/native-authentication-gmail/templates/after_google.html b/examples/native-authentication-gmail/templates/after_google.html similarity index 100% rename from examples/examples-new/native-authentication-gmail/templates/after_google.html rename to examples/native-authentication-gmail/templates/after_google.html diff --git a/examples/examples-new/native-authentication-gmail/templates/base.html b/examples/native-authentication-gmail/templates/base.html similarity index 100% rename from examples/examples-new/native-authentication-gmail/templates/base.html rename to examples/native-authentication-gmail/templates/base.html diff --git a/examples/examples-new/native-authentication-gmail/templates/before_google.html b/examples/native-authentication-gmail/templates/before_google.html similarity index 100% rename from examples/examples-new/native-authentication-gmail/templates/before_google.html rename to examples/native-authentication-gmail/templates/before_google.html diff --git a/examples/examples-new/webhooks/README.md b/examples/webhooks/README.md similarity index 100% rename from examples/examples-new/webhooks/README.md rename to examples/webhooks/README.md diff --git a/examples/examples-new/webhooks/config.json b/examples/webhooks/config.json similarity index 100% rename from examples/examples-new/webhooks/config.json rename to examples/webhooks/config.json diff --git a/examples/examples-new/webhooks/requirements.txt b/examples/webhooks/requirements.txt similarity index 100% rename from examples/examples-new/webhooks/requirements.txt rename to examples/webhooks/requirements.txt diff --git a/examples/examples-new/webhooks/server.py b/examples/webhooks/server.py similarity index 100% rename from examples/examples-new/webhooks/server.py rename to examples/webhooks/server.py diff --git a/examples/examples-new/webhooks/templates/base.html b/examples/webhooks/templates/base.html similarity index 100% rename from examples/examples-new/webhooks/templates/base.html rename to examples/webhooks/templates/base.html diff --git a/examples/examples-new/webhooks/templates/index.html b/examples/webhooks/templates/index.html similarity index 100% rename from examples/examples-new/webhooks/templates/index.html rename to examples/webhooks/templates/index.html From 65354106ba4e005d0bd996b12afe03a7b05d7e03 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 17:57:01 -0400 Subject: [PATCH 119/451] Correct string formatting --- nylas/client/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nylas/client/client.py b/nylas/client/client.py index d67109ca..aaf92d84 100644 --- a/nylas/client/client.py +++ b/nylas/client/client.py @@ -28,7 +28,7 @@ def _validate(response): print("{method} {url} ({body}) => {status}: {text}".format( method=response.request.method, url=response.request.url, - data=response.request.body, + body=response.request.body, status=response.status_code, text=response.text, )) From 07df92c96e69277b1ed113887bf1b1f0d5c456f5 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Mon, 7 Aug 2017 18:18:40 -0400 Subject: [PATCH 120/451] Remove duplicate header --- examples/hosted-oauth/templates/before_authorized.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/hosted-oauth/templates/before_authorized.html b/examples/hosted-oauth/templates/before_authorized.html index 4488e7a1..9a7215ba 100644 --- a/examples/hosted-oauth/templates/before_authorized.html +++ b/examples/hosted-oauth/templates/before_authorized.html @@ -1,7 +1,5 @@ {% extends "base.html" %} {% block body %} -

Nylas Hosted OAuth Example

- {% if not insecure_override %}