Skip to content

Commit

Permalink
Merge pull request #88 from singingwolfboy/datetime-attrs
Browse files Browse the repository at this point in the history
Convert timestamp fields to datetime fields
  • Loading branch information
singingwolfboy authored Aug 9, 2017
2 parents aff310a + a11c56b commit 871cc23
Show file tree
Hide file tree
Showing 9 changed files with 222 additions and 25 deletions.
2 changes: 1 addition & 1 deletion examples/webhooks/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def process_delta(delta):
"""
kwargs = {
"type": delta["type"],
"date": datetime.datetime.fromtimestamp(delta["date"]),
"date": datetime.datetime.utcfromtimestamp(delta["date"]),
"object_id": delta["object_data"]["id"],
}
print(" * {type} at {date} with ID {object_id}".format(**kwargs))
Expand Down
39 changes: 26 additions & 13 deletions nylas/client/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from __future__ import print_function
import sys
import json
from os import environ
from base64 import b64encode
import json
try:
from json import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError

import requests
from urlobject import URLObject
from six.moves.urllib.parse import urlencode
Expand All @@ -14,10 +19,7 @@
Label, Draft
)
from nylas.client.errors import APIClientError, ConnectionError, STATUS_MAP
try:
from json import JSONDecodeError
except ImportError:
JSONDecodeError = ValueError
from nylas.utils import convert_datetimes_to_timestamps

DEBUG = environ.get('NYLAS_CLIENT_DEBUG')
API_SERVER = "https://api.nylas.com"
Expand Down Expand Up @@ -256,7 +258,10 @@ def _get_resources(self, cls, extra=None, **filters):
postfix
)

url = str(URLObject(url).add_query_params(filters.items()))
converted_filters = convert_datetimes_to_timestamps(
filters, cls.datetime_filter_attrs,
)
url = str(URLObject(url).add_query_params(converted_filters.items()))
response = self._get_http_session(cls.api_root).get(url)
results = _validate(response).json()
return [
Expand All @@ -280,7 +285,10 @@ def _get_resource_raw(self, cls, id, extra=None,
url = "{}/a/{}/{}/{}{}".format(self.api_server, self.app_id,
cls.collection_name, id, postfix)

url = str(URLObject(url).add_query_params(filters.items()))
converted_filters = convert_datetimes_to_timestamps(
filters, cls.datetime_filter_attrs,
)
url = str(URLObject(url).add_query_params(converted_filters.items()))

response = self._get_http_session(cls.api_root).get(url, headers=headers)
return _validate(response)
Expand Down Expand Up @@ -311,10 +319,10 @@ def _create_resource(self, cls, data, **kwargs):
if cls == File:
response = session.post(url, files=data)
else:
data = json.dumps(data)
converted_data = convert_datetimes_to_timestamps(data, cls.datetime_attrs)
headers = {'Content-Type': 'application/json'}
headers.update(self.session.headers)
response = session.post(url, data=data, headers=headers)
response = session.post(url, json=converted_data, headers=headers)

result = _validate(response).json()
if cls.collection_name == 'send':
Expand All @@ -332,10 +340,13 @@ def _create_resources(self, cls, data):
if cls == File:
response = session.post(url, files=data)
else:
data = json.dumps(data)
converted_data = [
convert_datetimes_to_timestamps(datum, cls.datetime_attrs)
for datum in data
]
headers = {'Content-Type': 'application/json'}
headers.update(self.session.headers)
response = session.post(url, data=data, headers=headers)
response = session.post(url, json=converted_data, headers=headers)

results = _validate(response).json()
return [cls.create(self, **x) for x in results]
Expand Down Expand Up @@ -363,7 +374,8 @@ def _update_resource(self, cls, id, data, **kwargs):

session = self._get_http_session(cls.api_root)

response = session.put(url, json=data)
converted_data = convert_datetimes_to_timestamps(data, cls.datetime_attrs)
response = session.put(url, json=converted_data)

result = _validate(response).json()
return cls.create(self, **result)
Expand All @@ -390,9 +402,10 @@ def _call_resource_method(self, cls, id, method_name, data):
URLObject(self.api_server)
.with_path(url_path)
)
converted_data = convert_datetimes_to_timestamps(data, cls.datetime_attrs)

session = self._get_http_session(cls.api_root)
response = session.post(url, json=data)
response = session.post(url, json=converted_data)

result = _validate(response).json()
return cls.create(self, **result)
37 changes: 37 additions & 0 deletions nylas/client/restful_models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from datetime import datetime

from nylas.client.restful_model_collection import RestfulModelCollection
from nylas.client.errors import FileUploadError
from nylas.utils import timestamp_from_dt
from six import StringIO

# pylint: disable=attribute-defined-outside-init


class NylasAPIObject(dict):
attrs = []
datetime_attrs = {}
datetime_filter_attrs = {}
# The Nylas API holds most objects for an account directly under '/',
# but some of them are under '/a' (mostly the account-management
# and billing code). api_root is a tiny metaprogramming hack to let
Expand Down Expand Up @@ -44,6 +49,9 @@ def create(cls, api, **kwargs):
attr = attr_name[1:]
if attr in kwargs:
obj[attr_name] = kwargs[attr]
for dt_attr, ts_attr in cls.datetime_attrs.items():
if obj.get(ts_attr):
obj[dt_attr] = datetime.utcfromtimestamp(obj[ts_attr])
if 'id' not in kwargs:
obj['id'] = None

Expand All @@ -54,6 +62,9 @@ def as_json(self):
for attr in self.cls.attrs:
if hasattr(self, attr):
dct[attr] = getattr(self, attr)
for dt_attr, ts_attr in self.cls.datetime_attrs.items():
if self.get(dt_attr):
dct[ts_attr] = timestamp_from_dt(self[dt_attr])
return dct

def child_collection(self, cls, **filters):
Expand Down Expand Up @@ -83,6 +94,13 @@ class Message(NylasAPIObject):
"account_id", "object", "snippet", "starred", "subject",
"thread_id", "to", "unread", "starred", "_folder", "_labels",
"headers"]
datetime_attrs = {
"received_at": "date",
}
datetime_filter_attrs = {
"received_before": "received_before",
"received_after": "received_after",
}
collection_name = 'messages'

def __init__(self, api):
Expand Down Expand Up @@ -206,8 +224,21 @@ class Thread(NylasAPIObject):
attrs = ["draft_ids", "id", "message_ids", "account_id", "object",
"participants", "snippet", "subject", "subject_date",
"last_message_timestamp", "first_message_timestamp",
"last_message_received_timestamp", "last_message_sent_timestamp",
"unread", "starred", "version", "_folders", "_labels",
"received_recent_date"]
datetime_attrs = {
"first_message_at": "first_message_timestamp",
"last_message_at": "last_message_timestamp",
"last_message_received_at": "last_message_received_timestamp",
"last_message_sent_at": "last_message_sent_timestamp",
}
datetime_filter_attrs = {
"last_message_before": "last_message_before",
"last_message_after": "last_message_after",
"started_before": "started_before",
"started_after": "started_after",
}
collection_name = 'threads'

def __init__(self, api):
Expand Down Expand Up @@ -313,6 +344,9 @@ class Draft(Message):
"account_id", "object", "subject", "thread_id", "to",
"unread", "version", "file_ids", "reply_to_message_id",
"reply_to", "starred", "snippet", "tracking"]
datetime_attrs = {
"last_modified_at": "date",
}
collection_name = 'drafts'

def __init__(self, api, thread_id=None): # pylint: disable=unused-argument
Expand Down Expand Up @@ -407,6 +441,9 @@ class Event(NylasAPIObject):
"read_only", "when", "busy", "participants", "calendar_id",
"recurrence", "status", "master_event_id", "owner",
"original_start_time", "object", "message_id"]
datetime_attrs = {
"original_start_at": "original_start_time",
}
collection_name = 'events'

def __init__(self, api):
Expand Down
32 changes: 32 additions & 0 deletions nylas/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import division
from datetime import datetime


def timestamp_from_dt(dt, epoch=datetime(1970, 1, 1)):
"""
Convert a datetime to a timestamp.
https://stackoverflow.com/a/8778548/141395
"""
delta = dt - epoch
# return delta.total_seconds()
return delta.seconds + delta.days * 86400


def convert_datetimes_to_timestamps(data, datetime_attrs):
"""
Given a dictionary of data, and a dictionary of datetime attributes,
return a new dictionary that converts any datetime attributes that may
be present to their timestamped equivalent.
"""
if not data:
return data

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

return new_data
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
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
good-names=i,j,k,ex,Run,_,id,dt,db

# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
Expand Down
42 changes: 32 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ def mock_messages(mocked_responses, api_url, account_id):
}
],
"starred": False,
"unread": True
"unread": True,
"date": 1265077342,
}, {
"id": "1238",
"subject": "Test Message 2",
Expand All @@ -278,7 +279,8 @@ def mock_messages(mocked_responses, api_url, account_id):
}
],
"starred": False,
"unread": True
"unread": True,
"date": 1265085342,
}, {
"id": "12",
"subject": "Test Message 3",
Expand All @@ -292,7 +294,8 @@ def mock_messages(mocked_responses, api_url, account_id):
}
],
"starred": False,
"unread": False
"unread": False,
"date": 1265093842,
}
])
endpoint = re.compile(api_url + '/messages')
Expand Down Expand Up @@ -369,7 +372,11 @@ def mock_threads(mocked_responses, api_url, account_id):
"id": "abcd"
}],
"starred": True,
"unread": False
"unread": False,
"first_message_timestamp": 1451703845,
"last_message_timestamp": 1483326245,
"last_message_received_timestamp": 1483326245,
"last_message_sent_timestamp": 1483232461,
}
])
endpoint = re.compile(api_url + '/threads')
Expand All @@ -395,7 +402,11 @@ def mock_thread(mocked_responses, api_url, account_id):
"id": "abcd"
}],
"starred": True,
"unread": False
"unread": False,
"first_message_timestamp": 1451703845,
"last_message_timestamp": 1483326245,
"last_message_received_timestamp": 1483326245,
"last_message_sent_timestamp": 1483232461,
}
response_body = json.dumps(base_thrd)

Expand Down Expand Up @@ -451,7 +462,11 @@ def mock_labelled_thread(mocked_responses, api_url, account_id):
"account_id": account_id,
"object": "label"
}
]
],
"first_message_timestamp": 1451703845,
"last_message_timestamp": 1483326245,
"last_message_received_timestamp": 1483326245,
"last_message_sent_timestamp": 1483232461,
}
response_body = json.dumps(base_thread)

Expand Down Expand Up @@ -881,10 +896,17 @@ def mock_events(mocked_responses, api_url):
"title": "Pool party",
"location": "Local Community Pool",
"participants": [
"Alice",
"Bob",
"Claire",
"Dot",
{
"comment": None,
"email": "[email protected]",
"name": "Kelly Nylanaut",
"status": "noreply",
}, {
"comment": None,
"email": "[email protected]",
"name": "Sarah Nylanaut",
"status": "no",
},
]
}
])
Expand Down
11 changes: 11 additions & 0 deletions tests/test_drafts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
from datetime import datetime

import pytest
from nylas.client.errors import InvalidRequestError
from nylas.utils import timestamp_from_dt

# pylint: disable=len-as-condition


@pytest.mark.usefixtures("mock_drafts")
def test_draft_attrs(api_client):
draft = api_client.drafts.first()
expected_modified = datetime(2015, 8, 4, 10, 34, 46)
assert draft.last_modified_at == expected_modified
assert draft.date == timestamp_from_dt(expected_modified)


@pytest.mark.usefixtures(
"mock_draft_saved_response", "mock_draft_sent_response"
)
Expand Down
Loading

0 comments on commit 871cc23

Please sign in to comment.