-
Notifications
You must be signed in to change notification settings - Fork 0
/
EvernoteManager.py
330 lines (285 loc) · 12.5 KB
/
EvernoteManager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# EvernoteManager.py
# (c) 2009 Matt Ginzton, [email protected]
#
# Python class to wrap some of the details of connecting to the Evernote
# service.
#
# Changelog:
# - basic functionality: 2009/06/26
# - added tag functionality: 2009/06/27
# - taught to force well-formed note titles: 2009/07/12
# - converted to OAuth: 2012/10/15
import sys
#
# Force Python to notice local-embedded Evernote API libs
#
sys.path.append('./evernote-sdk-python/lib')
#
# Python modules we use
#
import traceback
import urllib
import urllib2
import xml.sax.saxutils
import webbrowser
import thrift.transport.THttpClient as THttpClient
import thrift.protocol.TBinaryProtocol as TBinaryProtocol
import evernote.edam.userstore.UserStore as UserStore
import evernote.edam.userstore.constants as UserStoreConstants
import evernote.edam.notestore.NoteStore as NoteStore
import evernote.edam.error.ttypes as Errors
import evernote.edam.limits.constants as Limits
import evernote.edam.type.ttypes as Types
from oauth_receiver import OAuthReceiver, parse_qs
client_name = "com.maddogsw.en_palmimport/1.2; Python"
# This decodes to my consumer API key from Evernote's developer support team.
# If you're clever enough to find this, good for you. Please get your own
# (it's not hard) or the Evernote gods may frown upon both of us.
_consumerKey = "metamatt"
_consumerSecret = "0sr93r08148qq487".encode('rot13')
class OAuthHelper:
def __init__(self, oauth_host):
self.oauth_host = oauth_host
self.oauth_url = 'https://%s/oauth' % oauth_host
def flow(self, config):
# OAuth for fun and profit.
# step 1: start webserver, so oauth redirect flow has somewhere to land
self.local_server = OAuthReceiver()
self._get_temp_credential()
# step 2: invoke user's webbrowser to authorize us, passing our webserver as callback url
if not self._authorize_temp_credential(config):
return (None, None)
# step 3: profit
(authToken, noteStoreUrl) = self._get_real_credential()
return (authToken, noteStoreUrl)
def _get_temp_credential(self):
# POST to request temporary credentials
# sample request: POST to https://sandbox.evernote.com/oauth?oauth_consumer_key=en_oauth_test&oauth_signature=1ca0956605acc4f2%26&oauth_signature_method=PLAINTEXT&oauth_timestamp=1288364369&oauth_nonce=d3d9446802a44259&oauth_callback=https%3A%2F%2Ffoo.com%2Fsettings%2Findex.php%3Faction%3DoauthCallback
# sample response: oauth_token=en_oauth_test.12BF8802654.687474703A2F2F6C6F63616C686F73742F7E736574682F4544414D576562546573742F696E6465782E7068703F616374696F6E3D63616C6C6261636B.1FFF88DC670B03799613E5AC956B6E6D&oauth_token_secret=&oauth_callback_confirmed=true
# need to keep oauth_token value
params = {
'oauth_consumer_key': _consumerKey,
'oauth_signature': _consumerSecret + '&', # Evernote does not use oauth_token_secret
'oauth_signature_method': 'PLAINTEXT',
'oauth_callback': self.local_server.url # The browser will redirect here upon authorization.
}
response = urllib2.urlopen(self.oauth_url, data = urllib.urlencode(params))
result = parse_qs(response.read())
self.temp_credential = result['oauth_token'][0]
def _authorize_temp_credential(self, config):
# browse to interactive authentication webapp, to authorize the temporary credential
# navigate to https://server/OAuth.action?oauth_token=<>
# browser will redirect to callback_url provided in initial credential request, providing the oauth token and verifier
self.local_server.start(self.temp_credential)
webbrowser.open_new_tab('https://%s/OAuth.action?oauth_token=%s' % (self.oauth_host, self.temp_credential))
self.oauth_verifier = self.local_server.wait(config)
return self.oauth_verifier is not None
def _get_real_credential(self):
# POST to exchange temporary authorized credential for real one
# sample request: POST to https://sandbox.evernote.com/oauth?oauth_consumer_key=en_oauth_test&oauth_signature=1ca0956605acc4f2%26&oauth_signature_method=PLAINTEXT&oauth_timestamp=1288364923&oauth_nonce=755d38e6d163e820&oauth_token=en_oauth_test.12BF8888B3F.687474703A2F2F6C6F63616C686F73742F7E736574682F4544414D576562546573742F696E6465782E7068703F616374696F6E3D63616C6C6261636B.C3118B25D0F89531A375382BEEEDD421&oauth_verifier=DF427565AF5473BBE3D85D54FB4D63A4
# (with oauth_token and oauth_verifier from authorization)
# sample response: oauth_token=S%3Ds4%3AU%3Da1%3AE%3D12bfd68c6b6%3AC%3D12bf8426ab8%3AP%3D7%3AA%3Den_oauth_test%3AH%3D3df9cf6c0d7bc410824c80231e64dbe1&oauth_token_secret=&edam_noteStoreUrl=https%3A%2F%2Fsandbox.evernote.com%2Fedam%2Fnote%2Fshard%2Fs4&edam_userId=161
params = {
'oauth_consumer_key': _consumerKey,
'oauth_signature': _consumerSecret + '&',
'oauth_signature_method': 'PLAINTEXT',
'oauth_token': self.temp_credential,
'oauth_verifier': self.oauth_verifier
}
response = urllib.urlopen(self.oauth_url, data = urllib.urlencode(params))
# need to url-decode and keep the oauth_token and edam_noteStoreUrl values
result = parse_qs(response.read())
return (result['oauth_token'][0], result['edam_noteStoreUrl'][0])
class EvernoteManager:
def __init__(self, live = False):
# Instance variables
self.userStore = None # UserStore.Client we create to talk to UserStore service (before authentication)
self.authToken = None # Token returned from OAuth authentication and used for all NoteStore API calls
self.noteStoreUrl = None # URL for the specific user's NoteStore, encoding shard ID
self.noteStore = None # NoteStore.Client we create to talk to NoteStore service (after authentication)
self._tags = None # Memo-ized list of tags, built on demand in _GetTags
# Initialize the Evernote manager object, by default talking to Evernote's
# sandbox (testing) server. Specify live = True if you want the real server.
self._evernoteBaseHost = 'www.evernote.com' if live else 'sandbox.evernote.com'
def Connect(self):
# Connect to Evernote and check version
# Side effects: caches user store
# Result: tuple with success/fail as true/false, followed by error message if any
try:
userStoreUri = "https://" + self._evernoteBaseHost + "/edam/user"
print "Evernote service URL: " + userStoreUri
userStoreHttpClient = THttpClient.THttpClient(userStoreUri)
#userStoreHttpClient.setCustomHeader('User-Agent', client_name) # XXX not available in evernote-python-sdk?
userStoreProtocol = TBinaryProtocol.TBinaryProtocol(userStoreHttpClient)
self.userStore = UserStore.Client(userStoreProtocol)
versionOK = self.userStore.checkVersion(client_name,
UserStoreConstants.EDAM_VERSION_MAJOR,
UserStoreConstants.EDAM_VERSION_MINOR)
except:
traceback.print_exc(file=sys.stderr)
return [False, str(sys.exc_info()[1])]
if not versionOK:
return [False, "API version error"]
return [True, userStoreUri]
def is_authenticated(self):
# Result: True/False
if self.authToken:
try:
user = self.userStore.getUser(self.authToken)
return True
except Errors.EDAMUserException, ex:
print 'Authentication error: %d' % ex.errorCode
return False
def AuthenticateWithCachedToken(self, cached_token):
# Result: tuple with success/fail as true/false, followed by error message if any
self.authToken = cached_token
self.noteStoreUrl = self.userStore.getNoteStoreUrl(self.authToken)
if self.is_authenticated():
return [True, '']
else:
return [False, 'authentication failure']
def AuthenticateInteractively(self, config):
# Result: tuple with success/fail as true/false, followed by error message if any
auth_helper = OAuthHelper(self._evernoteBaseHost)
(self.authToken, self.noteStoreUrl) = auth_helper.flow(config)
if self.is_authenticated():
return [True, '']
else:
return [False, 'authentication failure']
def DiscardAuthentication(self):
self.authToken = None
self.noteStoreUrl = None
self.noteStore = None
def get_user_name(self):
user = self.userStore.getUser(self.authToken)
return user.username
def GetNoteStore(self):
# Returns note store
# Side effects: if note store not cached, opens it and caches result
# Result: note store
if not self.noteStore:
self._OpenNoteStore()
return self.noteStore
def _OpenNoteStore(self):
# Opens the note store (assumes not open)
# Side effects: caches result
# Result: none
noteStoreHttpClient = THttpClient.THttpClient(self.noteStoreUrl)
noteStoreProtocol = TBinaryProtocol.TBinaryProtocol(noteStoreHttpClient)
self.noteStore = NoteStore.Client(noteStoreProtocol)
def GetNotebooks(self):
# Returns notebooks for user after Connect and Authenticate
# Side effects: none
# Result: notebooks
noteStore = self.GetNoteStore()
notebooks = noteStore.listNotebooks(self.authToken)
return notebooks
def CreateNotebook(self, name):
noteStore = self.GetNoteStore()
notebook = Types.Notebook()
notebook.name = name
createdNotebook = noteStore.createNotebook(self.authToken, notebook)
return createdNotebook
def FindNotebook(self, name):
noteStore = self.GetNoteStore()
notebooks = noteStore.listNotebooks(self.authToken)
for notebook in notebooks:
if notebook.name == name:
return notebook
return None
def CreateNotePlaintext(self, notebook, title, body, date, tags):
# Evernote API expects that strings are XML-escaped UTF-8; we'll do the XML
# escaping here, but we expect that title and body are already well-formed
# Unicode strings encoded in UTF-8.
noteStore = self.GetNoteStore()
# Need to convert body to well-formed XML:
# - escape entities, etc
# - change \n to <br/>
# (Note that title remains a literal string, not XML.)
bodyXML = ""
for line in body.split('\n'):
bodyXML += xml.sax.saxutils.escape(line) + "<br/>"
note = Types.Note()
note.notebookGuid = notebook.guid
note.title = self._MakeEvernoteTitle(title)
note.content = '<?xml version="1.0" encoding="UTF-8"?>'
note.content += '<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml.dtd">'
note.content += '<en-note>'
note.content += bodyXML
note.content += '</en-note>'
note.created = date
note.updated = date
note.tagGuids = tags
createdNote = noteStore.createNote(self.authToken, note)
return createdNote
def LookupTags(self, tagNameList):
# Returns GUIDs of existing tags by name (input is list of strings, output is list of GUIDs)
# Side effects: creates any tags that didn't exist
# Return value: list of GUIDs, suitable for passing to CreateNotePlaintext
guids = []
for tagName in tagNameList:
if len(tagName) == 0: # allow and ignore list to contain empty tag names
continue
# This is O(M*N), M is number of tags in EN account, N is number of tags in incoming list
# Could be made O(N) but doesn't seem worth the trouble.
guid = self.FindTagByName(tagName)
if not guid:
guid = self.CreateTagByName(tagName)
guids.append(guid)
return guids
def FindTagByName(self, tagName):
# Looks for existing tag by name; returns its GUID
# Side effects: populates tag cache
# Return value: GUID of existing tag, or None
tags = self._GetTags()
for tag in tags:
# Note that Evernote tags are not case sensitive
if tag.name.lower() == tagName.lower():
return tag.guid
return None
def CreateTagByName(self, tagName):
# Creates a new tag by name; returns its GUID
# Side effects: creates tag, adjusts tag cache
# Return value: GUID of new tag
noteStore = self.GetNoteStore()
tag = Types.Tag()
tag.name = tagName
tag = noteStore.createTag(self.authToken, tag)
tags = self._GetTags()
tags.append(tag)
return tag.guid
def _GetTags(self):
# Returns tags for note store
# Side effects: caches tags
# Result: tag list
if self._tags == None:
noteStore = self.GetNoteStore()
self._tags = noteStore.listTags(self.authToken)
return self._tags
def _MakeEvernoteTitle(self, title):
# input: string (probably first line of note)
# output: string, legal as Evernote title
# Evernote's "Struct: Note" docs for the title field say:
# The subject of the note. Can't begin or end with a space.
# Length: EDAM_NOTE_TITLE_LEN_MIN - EDAM_NOTE_TITLE_LEN_MAX
title = title.strip()
if len(title) < Limits.EDAM_NOTE_TITLE_LEN_MIN:
return "(Untitled)"
if len(title) > Limits.EDAM_NOTE_TITLE_LEN_MAX:
title = title[ : Limits.EDAM_NOTE_TITLE_LEN_MAX]
return title
# Basic unit test harness
if __name__ == "__main__":
EN = EvernoteManager()
EN.Connect()
class Config:
canceled = False
EN.AuthenticateInteractively(Config())
notebooks = EN.GetNotebooks()
print str(len(notebooks)) + " notebooks found"
for n in notebooks:
print "Notebook: " + n.name
tags = EN._GetTags()
print str(len(tags)) + " tags found"
for t in tags:
print "Tag: " + t.name