├── tests ├── __init__.py ├── test_onenote.py └── test_auth.py ├── README.md ├── onenoteauth.py ├── onenotecli.py └── onenote.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_onenote.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import os 4 | import onenote 5 | from datetime import datetime 6 | from unittest import mock 7 | 8 | TEST_SES_FILE = 'test.ses' 9 | 10 | class TestPage(object): 11 | def __init__(self, name): 12 | self.data = {'id' : name + '_id', 13 | 'title' : name + '_title', 14 | 'parentSection' : {'id' : name + '_id', 15 | 'name' : name + '_name'}, 16 | 'createdTime' : datetime(2016,1,1).isoformat(), 17 | 'lastModifiedTime' : datetime(2016,2,1).isoformat() 18 | } 19 | 20 | 21 | 22 | class OneNoteTestCase(unittest.TestCase): 23 | def setUp(self): 24 | t_access_token = '123' 25 | t_refresh_token = '456' 26 | t_code = 'CODE' 27 | 28 | t_client_id = 't_id' 29 | t_client_secret = 't_secret' 30 | t_scope = 't_scope' 31 | t_redirect_url = 't_redir' 32 | #prepare session file 33 | ses_data = {'client_id' : t_client_id, 34 | 'client_secret' : t_client_secret, 35 | 'redirect_url' : t_redirect_url, 36 | 'scope' : t_scope, 37 | 'access_token' : t_access_token, 38 | 'refresh_token' : t_refresh_token} 39 | 40 | with open(TEST_SES_FILE,'w') as f: 41 | json.dump(ses_data,f) 42 | 43 | 44 | def tearDown(self): 45 | os.remove(TEST_SES_FILE) 46 | 47 | 48 | @mock.patch('onenote.OneNote.onenote_request') 49 | def test_get_pages(self, mock_onenote_request): 50 | """ 51 | get_pages test 52 | """ 53 | 54 | p0 = TestPage('p0') 55 | p1 = TestPage('p1') 56 | p2 = TestPage('p2') 57 | 58 | t_pages = [p0.data, p1.data, p2.data] 59 | t_status = 200 60 | 61 | # setup reques.post mock 62 | response = (t_status, {'value': t_pages}) 63 | mock_onenote_request.return_value = response 64 | 65 | #run test 66 | o = onenote.OneNote(ses_file = TEST_SES_FILE) 67 | r_status = o.get_pages() 68 | 69 | # check results 70 | mock_onenote_request.assert_called_once_with('https://www.onenote.com/api/v1.0/me/notes/pages') 71 | self.assertEqual(r_status,t_status) 72 | self.assertEqual(o.pages[0].name,'p0_title') 73 | self.assertEqual(o.pages[1].name,'p1_title') 74 | self.assertEqual(o.pages[2].name,'p2_title') 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OneNote command-line access tools 2 | ---------- 3 | ## Installation 4 | ## Authentication 5 | ```python 6 | import onenote 7 | o = onenote.OneNote(client_id="your_client_id", 8 | client_secret="your_client_secret", 9 | redirect_url="your_redirect_url", 10 | scope= 'office.onenote wl.signin wl.offline_access') 11 | 12 | o.authenticate() 13 | 14 | ``` 15 | On successfull completion authenticate() method stores all OAuth session data in file (by default .onenote.ses). 16 | 17 | If you run OneNote constructor without any arguments then session data will be loaded from .onenote.ses 18 | 19 | ```python 20 | import onenote 21 | o = onenote.OneNote() 22 | 23 | ``` 24 | ## Usage examples 25 | 26 | ```python 27 | # get all notebooks 28 | o.get_notebooks() 29 | print(o.notebooks) 30 | 31 | # get all sections 32 | o.get_sections() 33 | print(o.sections) 34 | 35 | # get all pages 36 | o.get_pages() 37 | print(o.pages) 38 | 39 | # get page content (HTML) 40 | o.get_pages() 41 | n = 1 # page number in o.pages list 42 | page = o.get_page_content(o.pages[n]) 43 | 44 | # get page content (Markdown) 45 | o.get_pages() 46 | n = 1 # page number in o.pages list 47 | page = o.get_page_content_md(o.pages[n]) 48 | 49 | ``` 50 | 51 | ## Command line client usage 52 | 53 | 54 | ```bash 55 | # authentication 56 | $python onenotecli.py --auth --client_id= \ 57 | --client_secret= --redirect_url= \ 58 | --scope='office.onenote wl.signin wl.offline_access' 59 | 60 | # get OneNote elemebts in tree-like view 61 | $python onenotecli.py --tree 62 | 63 | # print list of the all pages 64 | $ python onenotecli.py -p 65 | 66 | # print list of the all pages in long format 67 | $ python onenotecli.py -pl 68 | 69 | # print list of the all notebooks 70 | $ python onenotecli.py -n 71 | 72 | # print list of the all sections 73 | $ python onenotecli.py -s 74 | 75 | # print content of the page 76 | $ python onenotecli.py -c 77 | 78 | 79 | ``` 80 | ## How to create new page 81 | 82 | To create new page on OneNote Online you have to make an authentication with the scope "office.onenote_update" 83 | ```bash 84 | $python onenotecli.py --auth --client_id= \ 85 | --client_secret= --redirect_url= \ 86 | --scope='office.onenote_update wl.signin wl.offline_access' 87 | 88 | # create a page with the content from file (the content has markdown format) 89 | $ python onenotecli.py --create-page= --in-section= \ 90 | --from-file= 91 | 92 | # create a page with the content from sdtin (the content has markdown format) 93 | $ python onenotecli.py --create-page= --in-section= 94 | Enter page content (press CTRL-D to complete): 95 | # Hello, world! 96 | * item1 97 | * item2 98 | CTRL-D 99 | Creating page... 100 | Page in section Work is created successfully (201) 101 | 102 | 103 | 104 | 105 | ``` 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | import json 4 | import os 5 | from unittest import mock 6 | 7 | import onenote 8 | 9 | #logging.basicConfig(level=logging.INFO) 10 | 11 | TEST_SES_FILE = 'test.ses' 12 | TEST_SAVE_FILE = 'test.save' 13 | 14 | auth_url = 'https://login.live.com/oauth20_token.srf' 15 | 16 | 17 | class OneNoteAuthTestCase(unittest.TestCase): 18 | # def setUp(self): 19 | # with open(TEST_SES_FILE,'w') as f: 20 | # json.dump({'1':'2'},f) 21 | 22 | def tearDown(self): 23 | os.remove(TEST_SES_FILE) 24 | 25 | 26 | @mock.patch('requests.post') 27 | @mock.patch('onenote.OneNote.auth_get_code') 28 | def test_create_instance_with_auth(self, mock_get_code, mock_post): 29 | """ 30 | Authentication test 31 | """ 32 | t_access_token = '123' 33 | t_refresh_token = '456' 34 | t_code = 'CODE' 35 | 36 | t_client_id = 't_id' 37 | t_client_secret = 't_secret' 38 | t_scope = 't_scope' 39 | t_redirect_url = 't_redir' 40 | 41 | 42 | # setup reques.post mock 43 | mock_post_responce = mock.Mock() 44 | post_resp_data = {'access_token' : t_access_token, 45 | 'refresh_token' : t_refresh_token} 46 | mock_post_responce.json.return_value = post_resp_data 47 | mock_post_responce.status_code = 200 48 | mock_post.return_value = mock_post_responce 49 | 50 | #setup auth_get_code mock 51 | mock_get_code.return_value = t_code 52 | 53 | #run test 54 | o = onenote.OneNote(client_id = t_client_id, 55 | client_secret = t_client_secret, 56 | scope = t_scope, 57 | redirect_url = t_redirect_url, 58 | ses_file = TEST_SES_FILE, 59 | save_file = TEST_SES_FILE 60 | ) 61 | result = o.authenticate() 62 | 63 | mock_get_code.assert_called_with() 64 | mock_post.assert_called_with(auth_url, headers={'Content-Type': 'application/x-www-form-urlencoded'}, 65 | data={'redirect_url': t_redirect_url, 66 | 'client_id': t_client_id, 'client_secret': t_client_secret, 67 | 'grant_type': 'authorization_code', 68 | 'code': t_code}) 69 | 70 | self.assertEqual(result,t_access_token) 71 | 72 | with open(TEST_SES_FILE,'r') as f: 73 | ses_data = json.load(f) 74 | 75 | self.assertEqual(ses_data['client_id'], t_client_id) 76 | self.assertEqual(ses_data['client_secret'], t_client_secret) 77 | self.assertEqual(ses_data['redirect_url'], t_redirect_url) 78 | self.assertEqual(ses_data['scope'], t_scope) 79 | self.assertEqual(ses_data['access_token'], t_access_token) 80 | self.assertEqual(ses_data['refresh_token'], t_refresh_token) 81 | 82 | 83 | @mock.patch('requests.post') 84 | @mock.patch('onenote.OneNote.auth_get_code') 85 | def test_create_instance_wo_auth(self, mock_get_code, mock_post): 86 | """ 87 | Load saved session test 88 | """ 89 | t_access_token = '123' 90 | t_refresh_token = '456' 91 | t_code = 'CODE' 92 | 93 | t_client_id = 't_id' 94 | t_client_secret = 't_secret' 95 | t_scope = 't_scope' 96 | t_redirect_url = 't_redir' 97 | 98 | #prepare session file 99 | ses_data = {'client_id' : t_client_id, 100 | 'client_secret' : t_client_secret, 101 | 'redirect_url' : t_redirect_url, 102 | 'scope' : t_scope, 103 | 'access_token' : t_access_token, 104 | 'refresh_token' : t_refresh_token} 105 | with open(TEST_SES_FILE,'w') as f: 106 | json.dump(ses_data,f) 107 | 108 | # setup reques.post mock 109 | mock_post_responce = mock.Mock() 110 | mock_post_responce.json.return_value = {} 111 | mock_post_responce.status_code = 401 112 | mock_post.return_value = mock_post_responce 113 | 114 | #setup auth_get_code mock 115 | mock_get_code.return_value = t_code 116 | 117 | #run test 118 | o = onenote.OneNote(ses_file = TEST_SES_FILE) 119 | result = o.get_token() 120 | 121 | # check results 122 | self.assertEqual(mock_get_code.called, False) 123 | self.assertEqual(mock_post.called, False) 124 | self.assertEqual(result,t_access_token) 125 | 126 | with open(TEST_SES_FILE,'r') as f: 127 | ses_data = json.load(f) 128 | 129 | self.assertEqual(ses_data['client_id'], t_client_id) 130 | self.assertEqual(ses_data['client_secret'], t_client_secret) 131 | self.assertEqual(ses_data['redirect_url'], t_redirect_url) 132 | self.assertEqual(ses_data['scope'], t_scope) 133 | self.assertEqual(ses_data['access_token'], t_access_token) 134 | self.assertEqual(ses_data['refresh_token'], t_refresh_token) 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /onenoteauth.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | import socket 3 | import json 4 | import re 5 | import requests 6 | import logging 7 | import os 8 | import stat 9 | 10 | CODE_URL = 'https://login.live.com/oauth20_authorize.srf?\ 11 | response_type=code&client_id=%s&redirect_url=%s&scope=%s' 12 | 13 | HOST = '' 14 | PORT = 8085 15 | 16 | RESP_200 = """\ 17 | HTTP/1.1 200 OK 18 | Content-type: text/html 19 | 20 | 21 | """ 22 | 23 | 24 | class OneNoteAuth(object): 25 | SESSION_FILE = '.onenote.ses' 26 | SES_KEYS = ['client_id', 27 | 'client_secret', 28 | 'access_token', 29 | 'refresh_token', 30 | 'scope', 31 | 'redirect_url'] 32 | 33 | def __init__(self, ses_file=SESSION_FILE, client_id=None, 34 | client_secret=None, scope=None, redirect_url=None): 35 | logging.info('OneNoteAuth __init__: client_id=%s,\ 36 | client_secret=%s, scope=%s,\ 37 | redirect_url=%s' % (client_id, client_secret, 38 | scope, redirect_url)) 39 | self.ses_file = ses_file 40 | self.auth_cfg = { 41 | 'client_id': None, 42 | 'client_secret': None, 43 | 'scope': None, 44 | 'access_token': None, 45 | 'refresh_token': None, 46 | 'redirect_url': None} 47 | 48 | if client_id and client_secret and scope and redirect_url: 49 | self.auth_cfg['client_id'] = client_id 50 | self.auth_cfg['client_secret'] = client_secret 51 | self.auth_cfg['scope'] = scope 52 | self.auth_cfg['redirect_url'] = redirect_url 53 | self.auth_cfg['access_token'] = None 54 | self.auth_cfg['refresh_token'] = None 55 | else: 56 | data = self.load_session() 57 | if data: 58 | self.auth_cfg['client_id'] = data['client_id'] 59 | self.auth_cfg['client_secret'] = data['client_secret'] 60 | self.auth_cfg['scope'] = data['scope'] 61 | self.auth_cfg['access_token'] = data['access_token'] 62 | self.auth_cfg['refresh_token'] = data['refresh_token'] 63 | self.auth_cfg['redirect_url'] = data['redirect_url'] 64 | 65 | def load_session(self): 66 | logging.info('load_session') 67 | data = None 68 | try: 69 | with open(self.ses_file, 'r') as f: 70 | data = json.load(f) 71 | except: 72 | logging.warning("%s file is missign or hasn't JSON format" % 73 | self.SESSION_FILE) 74 | return None 75 | 76 | if self.check_ses_data(data): 77 | return data 78 | 79 | def save_session(self): 80 | with open(self.ses_file, 'w') as f: 81 | json.dump(self.auth_cfg, f) 82 | os.chmod(self.ses_file, stat.S_IRUSR | stat.S_IWUSR) 83 | 84 | def check_ses_data(self, data): 85 | for k in self.SES_KEYS: 86 | if k not in data: 87 | return False 88 | 89 | return True 90 | 91 | def get_token(self): 92 | logging.info('get_token entry') 93 | if self.auth_cfg['access_token']: 94 | logging.info('get_token entry: cp0') 95 | return self.auth_cfg['access_token'] 96 | 97 | if self.auth_cfg['refresh_token']: 98 | logging.info('get_token entry: cp1') 99 | return self.get_refresh_token() 100 | 101 | else: 102 | logging.info('get_token entry: authenticate') 103 | return self.authenticate() 104 | 105 | def authenticate(self): 106 | logging.info('authenticate') 107 | # get authorization code 108 | code = self.auth_get_code() 109 | if not code: 110 | return None 111 | 112 | # get access_token 113 | logging.info('get access_token') 114 | url = 'https://login.live.com/oauth20_token.srf' 115 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 116 | payload = { 117 | 'grant_type': 'authorization_code', 118 | 'client_id': self.auth_cfg['client_id'], 119 | 'client_secret': self.auth_cfg['client_secret'], 120 | 'code': code, 121 | 'redirect_url': self.auth_cfg['redirect_url'] 122 | } 123 | r = requests.post(url, headers=headers, data=payload) 124 | response = r.json() 125 | logging.info('get_token: status_code=%d, response=%s' % 126 | (r.status_code, response)) 127 | if r.status_code != 200: 128 | return None 129 | if 'refresh_token' in response: 130 | self.auth_cfg['refresh_token'] = response['refresh_token'] 131 | if 'access_token' in response: 132 | self.auth_cfg['access_token'] = response['access_token'] 133 | self.save_session() 134 | return self.auth_cfg['access_token'] 135 | 136 | def auth_get_code(self): 137 | logging.info('auth_get_code entry') 138 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 139 | s.bind((HOST, PORT)) 140 | s.listen(1) 141 | 142 | webbrowser.open(CODE_URL % (self.auth_cfg['client_id'], 143 | self.auth_cfg['redirect_url'], 144 | self.auth_cfg['scope'])) 145 | 146 | logging.info('Serving HTTP on port %s...' % PORT) 147 | conn, addr = s.accept() 148 | request = conn.recv(1024) 149 | http_response = RESP_200 150 | 151 | conn.sendall(bytes(http_response, encoding='utf-8')) 152 | conn.close() 153 | s.shutdown(socket.SHUT_RDWR) 154 | s.close() 155 | 156 | request_line, _ = request.decode().split('\r\n', 1) 157 | resp = re.search(r'GET /\?code=([^\s]+) ', request_line) 158 | logging.info('auth_get_code: resp=%s' % resp) 159 | if resp: 160 | logging.info('auth_get_code: code=%s' % resp.group(1)) 161 | return resp.group(1) 162 | else: 163 | return None 164 | 165 | def handle_401(self): 166 | self.auth_cfg['access_token'] = None 167 | self.get_token() 168 | 169 | def get_refresh_token(self): 170 | # get access_token 171 | self.auth_cfg['access_token'] = None 172 | url = 'https://login.live.com/oauth20_token.srf' 173 | headers = {'Content-Type': 'application/x-www-form-urlencoded'} 174 | payload = { 175 | 'grant_type': 'refresh_token', 176 | 'client_id': self.auth_cfg['client_id'], 177 | 'client_secret': self.auth_cfg['client_secret'], 178 | 'redirect_url': self.auth_cfg['redirect_url'], 179 | 'refresh_token': self.auth_cfg['refresh_token'] 180 | } 181 | r = requests.post(url, headers=headers, data=payload) 182 | response = r.json() 183 | if 'refresh_token' in response: 184 | self.auth_cfg['refresh_token'] = response['refresh_token'] 185 | else: 186 | self.auth_cfg['refresh_token'] = None 187 | 188 | if 'access_token' in response: 189 | self.auth_cfg['access_token'] = response['access_token'] 190 | else: 191 | self.auth_cfg['access_token'] = None 192 | 193 | return self.auth_cfg['access_token'] 194 | -------------------------------------------------------------------------------- /onenotecli.py: -------------------------------------------------------------------------------- 1 | import onenote 2 | import argparse 3 | import sys 4 | import html2text 5 | import logging 6 | import markdown2 7 | 8 | 9 | def main(args): 10 | 11 | """ 12 | Setup logging 13 | """ 14 | if args.loglevel: 15 | loglevel = args.loglevel 16 | numeric_level = getattr(logging, loglevel.upper(), None) 17 | if not isinstance(numeric_level, int): 18 | raise ValueError('Invalid log level: %s' % loglevel) 19 | logging.basicConfig(level=numeric_level) 20 | 21 | """ 22 | Create OneNote instance and provide authorization 23 | """ 24 | if args.authorize: 25 | if not args.client_id: 26 | print('--client_id option is needed for authorization') 27 | exit(1) 28 | if not args.client_secret: 29 | print('--client_secret option is needed for authorization') 30 | exit(1) 31 | if not args.redirect_url: 32 | print('--redirect_url option is needed for authorization') 33 | exit(1) 34 | if not args.scope: 35 | print('--scope option is needed for authorization') 36 | exit(1) 37 | 38 | onote = onenote.OneNote(client_id=args.client_id, 39 | client_secret=args.client_secret, 40 | redirect_url=args.redirect_url, 41 | scope=args.scope) 42 | onote.authenticate() 43 | 44 | else: 45 | onote = onenote.OneNote() 46 | 47 | """ 48 | Get some flags from args 49 | """ 50 | if args.update: 51 | loaded = False 52 | else: 53 | loaded = onote.load_structure() 54 | 55 | if args.by_time: 56 | to_sort = 'lastmod_time' 57 | else: 58 | to_sort = 'name' 59 | 60 | longformat = args.long 61 | is_html = args.html 62 | 63 | """ 64 | Print list of pages 65 | """ 66 | if args.pages: 67 | if not loaded: 68 | onote.get_structure() 69 | 70 | for i in sorted(onote.pages, key=lambda x: getattr(x, to_sort)): 71 | if longformat: 72 | print('%s %s \t[%s] (%s -> %s)' % 73 | (i.lastmod_time.strftime('%Y %b %0d %H:%M'), i.id, 74 | i.name, i.parent_entity.parent_name, i.parent_name)) 75 | else: 76 | print(i.name) 77 | 78 | """ 79 | Print list of sections 80 | """ 81 | if args.sections: 82 | if not loaded: 83 | onote.get_structure() 84 | 85 | for i in sorted(onote.sections, key=lambda x: getattr(x, to_sort)): 86 | if longformat: 87 | print('%s %s \t[%s] (%s)' % 88 | (i.lastmod_time.strftime('%Y %b %0d %H:%M'), i.id, 89 | i.name, i.parent_name)) 90 | else: 91 | print(i.name) 92 | 93 | """ 94 | Print list of notebooks 95 | """ 96 | if args.notebooks: 97 | if not loaded: 98 | onote.get_structure() 99 | 100 | for i in sorted(onote.notebooks, key=lambda x: getattr(x, to_sort)): 101 | if longformat: 102 | print('%s %s \t[%s]' % 103 | (i.lastmod_time.strftime('%Y %b %0d %H:%M'), 104 | i.id, i.name)) 105 | else: 106 | print(i.name) 107 | 108 | """ 109 | Print content of the page selected by name. 110 | Default output format is markdown. 111 | --html - output in html format 112 | """ 113 | if args.content: 114 | if not loaded: 115 | onote.get_structure() 116 | page = onote.get_item(onote.pages, 'name', args.content) 117 | if page: 118 | if is_html: 119 | print(onote.get_page_content(page[0])) 120 | else: 121 | print(html2text.html2text(onote.get_page_content(page[0]))) 122 | else: 123 | print("Error: page '%s' isn't found" % args.content) 124 | 125 | """ 126 | Print the OneNote structure in tree-like format 127 | """ 128 | if args.tree: 129 | if not loaded: 130 | onote.get_structure() 131 | 132 | i_n = 0 133 | for n in onote.notebooks: 134 | i_n += 1 135 | if i_n == len(onote.notebooks): 136 | last_n = True 137 | isymb = chr(0x2514) 138 | else: 139 | last_n = False 140 | isymb = chr(0x251C) 141 | 142 | prefix = '%s ' % (isymb+2*chr(0x2500)) 143 | line = prefix + n.name 144 | print(line) 145 | 146 | i_s = 0 147 | for s in n.children: 148 | i_s += 1 149 | if i_s == len(n.children): 150 | last_s = True 151 | isymb = chr(0x2514) 152 | else: 153 | last_s = False 154 | isymb = chr(0x251C) 155 | 156 | if last_n: 157 | preprefix = 4*' ' 158 | else: 159 | preprefix = chr(0x2502) + 3*' ' 160 | 161 | prefix = '%s ' % (preprefix + isymb+2*chr(0x2500)) 162 | line = prefix + s.name 163 | print(line) 164 | 165 | i_p = 0 166 | for p in s.children: 167 | i_p += 1 168 | if i_p == len(s.children): 169 | isymb = chr(0x2514) 170 | else: 171 | isymb = chr(0x251C) 172 | 173 | if last_n and last_s: 174 | preprefix = 8*' ' 175 | elif last_n and not last_s: 176 | preprefix = 2*' ' + chr(0x2502) + 3*' ' 177 | elif not last_n and last_s: 178 | preprefix = chr(0x2502) + 7*' ' 179 | else: 180 | preprefix = chr(0x2502) + 3*' '+chr(0x2502) + 4*' ' 181 | 182 | prefix = '%s ' % (preprefix + isymb+2*chr(0x2500)) 183 | line = prefix + p.name 184 | print(line) 185 | 186 | """ 187 | Post page 188 | """ 189 | if args.create: 190 | if not args.in_section: 191 | print('You should give a section name to create page in (--in-section=)') # noqa 192 | else: 193 | if args.file: 194 | print(args.file) 195 | with open(args.file,'r') as f: 196 | data = f.read() 197 | else: 198 | print("Enter page content (press CTRL-D to complete)") 199 | data = sys.stdin.read() 200 | print("Creating page...") 201 | status = onote.create_page(args.create, args.in_section, 202 | markdown2.markdown(data)) 203 | if status == 403: 204 | print('Error 403: You are not permitted to perform the reqiested operation') # noqa 205 | elif status == 401: 206 | print('Error 401: Authorization failed') 207 | else: 208 | print('Page %s in section %s is created successfully (%d)' % 209 | (args.create, args.in_section, status)) 210 | 211 | sys.exit(0) 212 | 213 | 214 | if __name__ == '__main__': 215 | parser = argparse.ArgumentParser() 216 | parser.add_argument('-p', '--pages', action='store_true', 217 | default=False, help='print the list of pages') 218 | 219 | parser.add_argument('-n', '--notebooks', action='store_true', 220 | default=False, help='print the list of notebooks') 221 | 222 | parser.add_argument('-s', '--sections', action='store_true', 223 | default=False, help='print the list of sections') 224 | 225 | parser.add_argument('-l', dest='long', action='store_true', 226 | default=False, help='use a long listing format') 227 | 228 | parser.add_argument('-t', dest='by_time', action='store_true', 229 | default=False, 230 | help='sort by modification time, last first') 231 | 232 | parser.add_argument('-c', '--page-content', dest='content', action='store', 233 | help='print content of the page') 234 | 235 | parser.add_argument('--html', dest='html', action='store_true', 236 | help='print content of the page in HTML format') 237 | 238 | parser.add_argument('--tree', dest='tree', action='store_true', 239 | help='print OneNote structure in tree-like format') 240 | 241 | parser.add_argument('-u', '--update', dest='update', action='store_true', 242 | help='update OneNote structure from server') 243 | 244 | parser.add_argument('--log', dest='loglevel', action='store', 245 | help='set loglevel (INFO, DEBUG etc') 246 | 247 | parser.add_argument('--create-page', dest='create', action='store', 248 | help='create page in secion (--in-section=)') 249 | 250 | parser.add_argument('--in-section', dest='in_section', action='store', 251 | help='section to create page in') 252 | 253 | parser.add_argument('--from-file', dest='file', action='store', 254 | help='file to create page') 255 | 256 | parser.add_argument('--auth', dest='authorize', action='store_true', 257 | help='run authorization in OneNote Online') 258 | 259 | parser.add_argument('--client_id', dest='client_id', action='store', 260 | help='client_id for authorization') 261 | 262 | parser.add_argument('--client_secret', dest='client_secret', 263 | action='store', 264 | help='client_secret for authorization') 265 | 266 | parser.add_argument('--redirect_url', dest='redirect_url', action='store', 267 | help='redirect_url for authorization') 268 | 269 | parser.add_argument('--scope', dest='scope', action='store', 270 | help='scope for authorization') 271 | 272 | 273 | args = parser.parse_args() 274 | main(args) 275 | -------------------------------------------------------------------------------- /onenote.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import dateutil.parser 3 | import logging 4 | import json 5 | import os 6 | import stat 7 | import datetime 8 | from html2text import html2text 9 | from onenoteauth import OneNoteAuth 10 | 11 | 12 | BASE_URL = 'https://www.onenote.com/api/v1.0/me/notes/' 13 | SAVE_FILE_DEFAULT = '.onenote.save' 14 | 15 | PAGE_TEMPLATE = """\ 16 | 17 | 18 | %s 19 | 20 | 21 | %s 22 | 23 | """ 24 | 25 | 26 | class OEntity(object): 27 | 28 | def __init__(self, type, id, name, parent_id, parent_name, 29 | created_time, lastmod_time, children, 30 | parent_entity): 31 | self.type = type 32 | self.id = id 33 | self.name = name 34 | self.parent_id = parent_id 35 | self.parent_name = parent_name 36 | self.created_time = created_time 37 | self.lastmod_time = lastmod_time 38 | self.children = children 39 | self.parent_entity = parent_entity 40 | 41 | def __repr__(self): 42 | return self.name 43 | 44 | def decode(self): 45 | return {'type': self.type, 46 | 'id': self.id, 47 | 'name': self.name, 48 | 'parent_id': self.parent_id, 49 | 'parent_name': self.parent_name, 50 | 'created_time': self.created_time.isoformat(), 51 | 'lastmod_time': self.lastmod_time.isoformat(), 52 | 'parent_entity': self.parent_entity.id if 53 | self.parent_entity is not None else None, 54 | 'children': []} 55 | 56 | 57 | class OneNote(OneNoteAuth): 58 | def __init__(self, save_file=SAVE_FILE_DEFAULT, *args, **kwargs): 59 | OneNoteAuth.__init__(self, *args, **kwargs) 60 | self.notebooks = [] 61 | self.sections = [] 62 | self.pages = [] 63 | self.save_file = save_file 64 | 65 | def onenote_request(self, url, post=False, body=''): 66 | logging.info('onenote_request: url=%s' % url) 67 | token = self.get_token() 68 | if not token: 69 | logging.warn('get_token failed') 70 | return None 71 | 72 | if post: 73 | headers = {'Authorization': 'Bearer %s' % self.get_token(), 74 | 'Content-Type': 'application/xhtml+xml'} 75 | else: 76 | headers = {'Authorization': 'Bearer %s' % self.get_token()} 77 | 78 | logging.info('onenote_request: headers=%s' % headers) 79 | if post: 80 | r = requests.post(url, headers=headers, data=body.encode('utf-8')) 81 | else: 82 | r = requests.get(url, headers=headers) 83 | if r.status_code == 401: 84 | self.handle_401() 85 | headers = {'Authorization': 'Bearer %s' % self.get_token()} 86 | if post: 87 | r = requests.post(url, headers=headers, 88 | data=body.encode('utf-8')) 89 | else: 90 | r = requests.get(url, headers=headers) 91 | logging.info('response=%s' % r) 92 | try: 93 | data = r.json() 94 | except ValueError: 95 | data = r.text 96 | 97 | return r.status_code, data 98 | 99 | def get_url(self, url): 100 | logging.info("get url") 101 | url = BASE_URL + url 102 | status_code, text = self.onenote_request(url) 103 | return status_code, text 104 | 105 | def get_notebooks(self): 106 | logging.info("get notebooks") 107 | self.notebooks = [] 108 | url = BASE_URL + 'notebooks' 109 | while url: 110 | status_code, text = self.onenote_request(url) 111 | logging.info("get_notebooks: response=%s" % text) 112 | if status_code == 200: 113 | for i in range(len(text['value'])): 114 | entity = text['value'][i] 115 | oe = OEntity( 116 | type='notebook', 117 | id=entity['id'], 118 | name=entity['name'], 119 | parent_id=None, 120 | parent_name=None, 121 | created_time=dateutil.parser.parse(entity['createdTime']), # noqa 122 | lastmod_time=dateutil.parser.parse(entity['lastModifiedTime']), # noqa 123 | children=[], 124 | parent_entity=None 125 | ) 126 | self.notebooks.append(oe) 127 | 128 | if '@odata.nextLink' in text: 129 | url = text['@odata.nextLink'] 130 | else: 131 | url = None 132 | 133 | return status_code 134 | 135 | def get_sections(self): 136 | logging.info("get sections") 137 | self.sections = [] 138 | url = BASE_URL + 'sections' 139 | while url: 140 | status_code, text = self.onenote_request(url) 141 | logging.info("get_sections: response=%s" % text) 142 | if status_code == 200: 143 | for i in range(len(text['value'])): 144 | entity = text['value'][i] 145 | oe = OEntity( 146 | type='section', 147 | id=entity['id'], 148 | name=entity['name'], 149 | parent_id=entity['parentNotebook']['id'], 150 | parent_name=entity['parentNotebook']['name'], 151 | created_time=dateutil.parser.parse(entity['createdTime']), # noqa 152 | lastmod_time=dateutil.parser.parse(entity['lastModifiedTime']), # noqa 153 | children=[], 154 | parent_entity=None 155 | ) 156 | self.sections.append(oe) 157 | 158 | if '@odata.nextLink' in text: 159 | url = text['@odata.nextLink'] 160 | else: 161 | url = None 162 | 163 | return status_code 164 | 165 | def get_pages(self): 166 | logging.info("get pages") 167 | self.pages = [] 168 | url = BASE_URL + 'pages' 169 | while url: 170 | status_code, text = self.onenote_request(url) 171 | logging.info("get_pages", text) 172 | logging.info("get_pages: response=%s" % text) 173 | if status_code == 200: 174 | for i in range(len(text['value'])): 175 | entity = text['value'][i] 176 | logging.info("entity %s" % entity) 177 | oe = OEntity( 178 | type='page', 179 | id=entity['id'], 180 | name=entity['title'], 181 | parent_id=entity['parentSection']['id'], 182 | parent_name=entity['parentSection']['name'], 183 | created_time=dateutil.parser.parse(entity['createdTime']), # noqa 184 | lastmod_time=dateutil.parser.parse(entity['lastModifiedTime']), # noqa 185 | children=[], 186 | parent_entity=None 187 | ) 188 | self.pages.append(oe) 189 | 190 | if '@odata.nextLink' in text: 191 | url = text['@odata.nextLink'] 192 | else: 193 | url = None 194 | 195 | return status_code 196 | 197 | def create_tree(self): 198 | # for each notebook create list of sections 199 | for n in self.notebooks: 200 | n.children = [] 201 | for s in self.sections: 202 | if s.parent_id == n.id: 203 | n.children.append(s) 204 | s.parent_entity = n 205 | 206 | # for each section create list of pages 207 | for n in self.sections: 208 | n.children = [] 209 | for s in self.pages: 210 | if s.parent_id == n.id: 211 | n.children.append(s) 212 | s.parent_entity = n 213 | 214 | def get_structure(self): 215 | self.get_notebooks() 216 | self.get_sections() 217 | self.get_pages() 218 | self.create_tree() 219 | self.save_structure() 220 | 221 | def get_item(self, items, attr, value): 222 | return [i for i in items if getattr(i, attr) == value] 223 | 224 | def get_page_content(self, page): 225 | ''' 226 | Get page content in HTML 227 | ''' 228 | logging.info("get page content") 229 | url = BASE_URL + 'pages/' + page.id + '/content' 230 | status_code, text = self.onenote_request(url) 231 | if status_code == 200: 232 | return text 233 | 234 | def get_page_content_md(self, page): 235 | ''' 236 | Get page content in Markdown 237 | ''' 238 | logging.info("get page content") 239 | url = BASE_URL + 'pages/' + page.id + '/content' 240 | status_code, text = self.onenote_request(url) 241 | if status_code == 200: 242 | return html2text(text) 243 | 244 | def save_structure(self): 245 | with open(self.save_file, 'w') as f: 246 | data = {'notebooks': [i.decode() for i in self.notebooks], 247 | 'sections': [i.decode() for i in self.sections], 248 | 'pages': [i.decode() for i in self.pages]} 249 | 250 | json.dump(data, f) 251 | os.chmod(self.ses_file, stat.S_IRUSR | stat.S_IWUSR) 252 | 253 | def load_structure(self): 254 | data = None 255 | try: 256 | with open(self.save_file, 'r') as f: 257 | data = json.load(f) 258 | except: 259 | logging.warning("%s file is missign or hasn't JSON format" % 260 | self.save_file) 261 | return None 262 | 263 | self.notebooks = [] 264 | self.sections = [] 265 | self.pages = [] 266 | 267 | for n in data['notebooks']: 268 | oe = OEntity( 269 | type='notebook', 270 | id=n['id'], 271 | name=n['name'], 272 | parent_id=None, 273 | parent_name=None, 274 | created_time=dateutil.parser.parse(n['created_time']), 275 | lastmod_time=dateutil.parser.parse(n['lastmod_time']), 276 | children=[], 277 | parent_entity=None 278 | ) 279 | self.notebooks.append(oe) 280 | 281 | for s in data['sections']: 282 | oe = OEntity( 283 | type='section', 284 | id=s['id'], 285 | name=s['name'], 286 | parent_id=s['parent_id'], 287 | parent_name=s['parent_name'], 288 | created_time=dateutil.parser.parse(s['created_time']), 289 | lastmod_time=dateutil.parser.parse(s['lastmod_time']), 290 | children=[], 291 | parent_entity=None 292 | ) 293 | self.sections.append(oe) 294 | 295 | for p in data['pages']: 296 | oe = OEntity( 297 | type='page', 298 | id=p['id'], 299 | name=p['name'], 300 | parent_id=p['parent_id'], 301 | parent_name=p['parent_name'], 302 | created_time=dateutil.parser.parse(p['created_time']), 303 | lastmod_time=dateutil.parser.parse(p['lastmod_time']), 304 | children=[], 305 | parent_entity=None 306 | ) 307 | self.pages.append(oe) 308 | 309 | self.create_tree() 310 | return True 311 | 312 | def create_page(self, title, section, text): 313 | logging.info("create_page") 314 | # get section id 315 | sec = self.get_item(self.sections, 'name', section) 316 | if not sec: 317 | logging.info("create_page: section %s isn't found" % section) 318 | return None 319 | 320 | url = BASE_URL + 'sections/' + sec[0].id + '/pages' 321 | 322 | body = PAGE_TEMPLATE % (title, 323 | datetime.datetime.now().isoformat(), 324 | text) 325 | 326 | status_code, text = self.onenote_request(url, post=True, body=body) 327 | 328 | return status_code 329 | --------------------------------------------------------------------------------