├── .gitignore
├── .tool-versions
├── .travis.yml
├── COPYING
├── MANIFEST.in
├── README.md
├── circle.yml
├── completion
├── bash_completion
│ └── _geeknote
└── zsh_completion
│ └── _geeknote
├── geeknote.rb
├── geeknote
├── __init__.py
├── argparser.py
├── config.py
├── editor.py
├── gclient.py
├── geeknote.py
├── gnsync.py
├── log.py
├── oauth.py
├── out.py
├── storage.py
└── tools.py
├── proxy_support.md
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
├── fixtures
│ └── Test Note with non-ascii.xml
├── helpers.py
├── pseudoedit.py
├── test_argparser.py
├── test_editor.py
├── test_gclient.py
├── test_geeknote.py
├── test_gnsync.py
├── test_out.py
├── test_sandbox.py
├── test_storage.py
└── test_tools.py
└── tox.ini
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 | *~
3 | # Packages
4 | *.egg
5 | *.egg-info
6 | .eggs
7 | dist
8 | build
9 | eggs
10 | parts
11 | bin
12 | var
13 | sdist
14 | develop-eggs
15 | .installed.cfg
16 | .idea
17 | .venv
18 | /venv/
19 |
20 | # Installer logs
21 | pip-log.txt
22 | installed_files.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 | .cache
28 | .pytest_cache/
29 |
30 | #Translations
31 | *.mo
32 |
33 | #Mr Developer
34 | .mr.developer.cfg
35 |
36 | #Mac files
37 | .DS_Store
38 |
39 | # virtualenv
40 | include/
41 | lib/
42 | local/
43 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | python 2.7.16
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | # geeknote uses features of python not present in 2.6
4 | # - "2.6"
5 | - "2.7"
6 | # The official Evernote SDK doesn't support Python 3 yet
7 | # Keep an eye on https://github.com/evernote/evernote-sdk-python3
8 | # - "3.2"
9 | # - "3.3"
10 | # - "3.4"
11 | # - "3.5"
12 | # - "3.5-dev" # 3.5 development branch
13 | # - "nightly" # currently points to 3.6-dev
14 |
15 | os:
16 | - linux
17 | # - osx
18 |
19 | install:
20 | - pip install -r requirements.txt
21 |
22 | # command to run tests
23 | script: python -m pytest
24 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md requirements.txt MANIFEST.in
2 | include completion/zsh_completion/_geeknote completion/bash_completion/_geeknote
3 | exclude *.orig *.pyc
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Archived
2 | ===
3 | I no longer maintain this repository. If you've come here looking for bug fixes, please check one of the more recently updated forks to see if they are more recently maintained.
4 |
5 |
6 | Important Note
7 | ===
8 | This is an updated version of geeknote for Python 3 using evernote-sdk-python3 (which is also in beta). Even though things are not throughly tested, basic functionality works.
9 |
10 | Geeknote for Evernote (or 印象笔记) [](https://travis-ci.org/jeffkowalski/geeknote) [](https://circleci.com/gh/jeffkowalski/geeknote)
11 | ===
12 |
13 | Geeknote is a command line client for Evernote that can be use on Linux, FreeBSD and OS X.
14 |
15 | It allows you to:
16 | * create notes in your Evernote account;
17 | * create tags, notebooks;
18 | * use Evernote search from the console using different filters;
19 | * edit notes directly in the console using any editor, such as nano, vim or mcedit;
20 | * synchronize your local files and directories with Evernote;
21 | * use Evernote with cron or any scripts.
22 |
23 | Geeknote is open source and written in Python. Geeknote can be used anywhere you have Python installed (even in Windows if you like).
24 |
25 | In this document we'll show how to work with Evernote's notes, notebooks, and tags using Geeknote and how to use Geeknote sync.
26 |
27 | ## Installation
28 | You can install Geeknote using [Homebrew](http://brew.sh/)/[Linuxbrew](https://github.com/Homebrew/linuxbrew), or from its source.
29 |
30 | ##### Homebrew installation
31 |
32 | ``` sh
33 | brew install --HEAD https://raw.githubusercontent.com/jeffkowalski/geeknote/master/geeknote.rb
34 | ```
35 |
36 | ##### Downloading and installing from source
37 |
38 | ``` sh
39 | # Install dependencies. (This example for Debian-based systems):
40 | sudo apt-get update; sudo apt-get -y install python-setuptools
41 | pip install wheel
42 |
43 | # Download the repository.
44 | git clone git://github.com/jeffkowalski/geeknote.git
45 |
46 | cd geeknote
47 |
48 | # Installation
49 |
50 | # - if you have only a python2 environment,
51 | python setup.py build
52 | pip install --upgrade .
53 |
54 | # - or, to force python2 in python3 environments
55 | python2 setup.py build
56 | pip2 install --upgrade .
57 | ```
58 |
59 | ##### Testing
60 | Geeknote has a non-destructive unit test suite with fair coverage.
61 |
62 | Ensure pytest framework is installed
63 | ``` sh
64 | pip install --upgrade pytest
65 | ```
66 |
67 | Execute the tests
68 | ``` sh
69 | py.test
70 | ```
71 |
72 | ##### Un-installation
73 |
74 | If originally installed via homebrew,
75 |
76 | ``` sh
77 | brew remove geeknote
78 | ```
79 |
80 | If originally installed from source,
81 |
82 | ``` sh
83 | pip uninstall geeknote
84 | ```
85 |
86 | ## Geeknote Settings
87 | ##### Authorizing Geeknote
88 | After installation, Geeknote must be authorized with Evernote prior to use. To authorize your Geeknote in Evernote launch the command *login*:
89 |
90 | ``` sh
91 | geeknote login
92 | ```
93 |
94 | This will start the authorization process. Geeknote will ask you to enter your credentials just once to generate access token, which will be saved in local database. Re-authorization is not required, if you won't decide to change user.
95 | After authorization you can start to work with Geeknote.
96 |
97 | ##### Logging out and changing users
98 | If you want to change Evernote user you should launch *logout* command:
99 |
100 | ``` sh
101 | geeknote logout
102 | ```
103 |
104 | Afterward, you can repeat the authorization step.
105 |
106 | ##### (Yìnxiàng Bǐjì notes)
107 |
108 | If you want to use Evernote's separate service in China Yìnxiàng Bǐjì (印象笔记),
109 | you need to set the environment variable `GEEKNOTE_BASE` to `yinxiang`.
110 |
111 | ``` sh
112 | GEEKNOTE_BASE=yinxiang geeknote login
113 | # or
114 | export GEEKNOTE_BASE=yinxiang
115 | geeknote ...commands...
116 | ```
117 |
118 | Yìnxiàng Bǐjì (印象笔记) is faster in China and it supports Chinese payment methods.
119 | Be aware that Yìnxiàng Bǐjì will not have support for sharing social features
120 | like Twitter or Facebook. Furthermore, since data are stored on servers in China,
121 | Chinese authorities have the right to access their data according to current
122 | regulations.
123 |
124 | For more information, see:
125 | [Evernote Launches Separate Chinese Service](https://blog.evernote.com/blog/2012/05/09/evernote-launches-separate-chinese-service/)
126 |
127 | ## Login with a developer token
128 |
129 | Geeknote requires a Developer token after an unsuccessful OAuth request.
130 |
131 | You can obtain one by following the next simple steps:
132 |
133 | - Create an API key for SANDBOX environment
134 | - Request your API key to be activated on production
135 | - Convert it to a personal token
136 |
137 | To do so, go to [Evernote FAQ](https://dev.evernote.com/support/faq.php#createkey) and refer to
138 | the section "How do I create an API key?". As directed, click on the
139 | "Get an API Key" button at the top of the page, and complete the
140 | revealed form. You'll then receive an e-mail with your key and
141 | secret.
142 |
143 | When you receive your key and secret, activate your key by following the
144 | instructions on the ["How do I copy my API key from Sandbox to www (production)?"](https://dev.evernote.com/support/faq.php#activatekey) section of the FAQ.
145 | Be sure to specify on the form that you're using the key for the "geeknote" application.
146 |
147 | Once your API key activation is processed by Evernote Developer
148 | Support, they will send you an email with further instructions on
149 | obtaining the personal token.
150 |
151 | ##### Examining your settings
152 |
153 | ``` sh
154 | $ geeknote settings
155 | Geeknote
156 | ******************************
157 | Version: 3.0
158 | App dir: /Users/username/.geeknote
159 | Error log: /Users/username/.geeknote/error.log
160 | Current editor: vim
161 | Markdown2 Extras: None
162 | Note extension: .markdown, .org
163 | ******************************
164 | Username: username
165 | Id: 11111111
166 | Email: example@gmail.com
167 | ```
168 |
169 | ##### Setting up the default editor
170 | You can edit notes within console editors in plain text or markdown format.
171 |
172 | You can setup the default editor you want to use. To check which editor is now set up as a default call:
173 |
174 | ``` sh
175 | geeknote settings --editor
176 | ```
177 |
178 | To change the default editor call:
179 |
180 | ``` sh
181 | geeknote settings --editor vim
182 | ```
183 |
184 | To use `gvim` you need to prevent forking from the terminal with `-f`:
185 |
186 | ``` sh
187 | geeknote settings --editor 'gvim -f'
188 | ```
189 |
190 | ###### Example
191 |
192 | ``` sh
193 | $ geeknote settings --editor
194 | Current editor is: nano
195 | $ geeknote settings --editor vim
196 | Editor successfully saved
197 | $ geeknote settings --editor
198 | Current editor is: vim
199 | ```
200 |
201 | ##### Enabling Markdown2 Extras
202 |
203 | You can enable [Markdown2 Extras](https://github.com/trentm/python-markdown2/wiki/Extras) you want to use while editing notes. To check which settings are currently enabled call:
204 |
205 | ``` sh
206 | geeknote settings --extras
207 | ```
208 | To change the Markdown2 Extras call:
209 |
210 | ```sh
211 | geeknote settings --extras "tables, footnotes"
212 | ```
213 | ###### Example
214 |
215 | ``` sh
216 | $ geeknote settings --extras
217 | current markdown2 extras is : ['None']
218 | $ geeknote settings --extras "tables, footnotes"
219 | Changes saved.
220 | $ geeknote settings --extras
221 | current markdown2 extras is : ['tables', 'footnotes']
222 | ```
223 |
224 | ## Working with Notes
225 | ### Notes: Creating notes
226 | The main functionality that we need is creating notes in Evernote.
227 |
228 | ##### Synopsis
229 |
230 | ``` sh
231 | geeknote create --title
232 | [--content ]
233 | [--tag ]
234 | [--created ]
235 | [--resource ]
236 | [--notebook ]
237 | [--reminder ]
238 | [--url ]
239 | ```
240 |
241 | ##### Options
242 |
243 | | Option | Argument | Description |
244 | |------------|----------|-------------|
245 | | ‑‑title | title | With this option we specify the title of new note we want to create. |
246 | | ‑‑content | content | Specify the content of new note. The content must not contain double quotes. |
247 | | ‑‑tag | tag | Specify tag that our note will have. May be repeated. |
248 | | ‑‑created | date | Set note creation date and time in either 'yyyy-mm-dd' or 'yyyy-mm-dd HH:MM' format. |
249 | | ‑‑resource | attachment filename, like: document.pdf |Specify file to be attached to the note. May be repeated. |
250 | | ‑‑notebook | notebook where to save | Specify the notebook where new note should be saved. This option is not required. If it isn't given, the note will be saved in default notebook. If notebook doesn't exist Geeknote will create it automatically. |
251 | | ‑‑reminder | date | Set reminder date and time in either 'yyyy-mm-dd' or 'yyyy-mm-dd HH:MM' format. Alternatively use TOMORROW and WEEK for 24 hours and a week ahead respectively, NONE for a reminder without a time. Use DONE to mark a reminder as completed. |
252 | | --urls | url | Set the URL for the note. |
253 | | --raw | | A flag signifying the content is in raw ENML format. |
254 | | --rawmd | | A flag signifying the content is in raw markdown format. |
255 |
256 | ##### Description
257 | This command allows us to create a new note in Evernote. Geeknote has designed for using in console, so we have some restrictions like inability to use double quotes in **--content** option. But there is a method to avoid it - use stdin stream or file synchronization, we show it later in documentation.
258 |
259 | ##### Examples
260 |
261 | Creating a new note with a PDF attachment:
262 |
263 | ``` sh
264 | geeknote create --title "Shopping list"
265 | --content "Don't forget to buy milk, turkey and chips."
266 | --resource shoppinglist.pdf
267 | --notebook "Family"
268 | --tag "shop" --tag "holiday" --tag "important"
269 | ```
270 |
271 | Creating a new note and editing content in editor (notice the lack of `content` argument):
272 |
273 | ``` sh
274 | geeknote create --title "Meeting with customer"
275 | --notebook "Meetings"
276 | --tag "projectA" --tag "important" --tag "report"
277 | --created "2015-10-23 14:30"
278 |
279 | ```
280 |
281 | ### Notes: Searching for notes in Evernote
282 |
283 | You can easily search notes in Evernote with Geeknote and output results in the console.
284 |
285 | ##### Synopsis
286 |
287 | ``` sh
288 | geeknote find --search
289 | [--tag ]
290 | [--notebook ]
291 | [--date ]
292 | [--count ]
293 | [--exact-entry]
294 | [--content-search]
295 | [--url-only]
296 | [--reminders-only]
297 | [--deleted-only]
298 | [--ignore-completed]
299 | [--with-tags]
300 | [--with-notebook]
301 | [--guid]
302 | ```
303 |
304 | ##### Description
305 |
306 | Use **find** to search through your Evernote notebooks, with options to search and print more detail. Geeknote remembers the result of the last search. So, you can use the ID number of the note's position for future actions.
307 | For example:
308 |
309 | ``` sh
310 | $ geeknote find --search "Shopping"
311 |
312 | Total found: 2
313 | 1 : 2006-06-02 2009-01-19 Grocery Shopping List
314 | 2 : 2015-02-22 2015-02-24 Gift Shopping List
315 |
316 | $ geeknote show 2
317 | ################### URL ###################
318 | NoteLink: https://www.evernote.com/shard/s1/nl/2079/7aecf253-c0d9-407e-b4e2-54cd5510ead6
319 | WebClientURL: https://www.evernote.com/Home.action?#n=7aecf253-c0d9-407e-b4e2-54cd5510ead6
320 | ################## TITLE ##################
321 | Gift Shopping List
322 | =================== META ==================
323 | Notebook: EverNote
324 | Created: 2015-02-22
325 | Updated: 2012-02-24
326 | |||||||||||||||| REMINDERS ||||||||||||||||
327 | Order: None
328 | Time: None
329 | Done: None
330 | ----------------- CONTENT -----------------
331 | Tags: shopping
332 | Socks
333 | Silly Putty
334 | Furby
335 | ```
336 |
337 | That will show you the note "Gift Shopping List".
338 |
339 | ##### Options
340 |
341 | | Option | Argument | Description |
342 | |--------------------|-----------------|-------------|
343 | | ‑‑search | text to find | Set the text to find. You can use "*" like this: *--search "Shop*"* |
344 | | ‑‑tag | tag | Filter by tag. May be repeated. |
345 | | ‑‑notebook | notebook | Filter by notebook. |
346 | | ‑‑date | date or range | Filter by date. You can set a single date in 'yyyy-mm-dd' format or a range with 'yyyy-mm-dd/yyyy-mm-dd' |
347 | | ‑‑count | how many results to show | Limits the number of displayed results. |
348 | | ‑‑content-search | | *find* command searches by note's title. If you want to search by note's content - set this flag. |
349 | | ‑‑exact-entry | | By default Geeknote has a smart search, so it searches fuzzy entries. But if you need exact entry, you can set this flag. |
350 | | ‑‑guid | | Show GUID of the note as substitute for result index. |
351 | | ‑‑ignore-completed | | Include only unfinished reminders. |
352 | | ‑‑reminders-only | | Include only notes with a reminder. |
353 | | ‑‑deleted-only | | Include only notes that have been **deleted/trashed**. |
354 | | ‑‑with-notebook | | Show notebook containing the note. |
355 | | ‑‑with-tags | | Show tags of the note after note title. |
356 | | ‑‑with-url | | Show results as a list of URLs to each note in Evernote's web-client. |
357 |
358 | ##### Examples
359 |
360 | ``` sh
361 | geeknote find --search "How to patch KDE2" --notebook "jokes" --date 2015-10-14/2015-10-28
362 | geeknote find --search "apt-get install apache nginx" --content-search --notebook "manual"
363 | ```
364 |
365 | ### Notes: Editing notes
366 |
367 | With Geeknote you can edit your notes in Evernote using any editor you like (nano, vi, vim, emacs, etc.)
368 |
369 | ##### Synopsis
370 |
371 | ``` sh
372 | geeknote edit --note
373 | [--title ]
374 | [--content ]
375 | [--resource ]
376 | [--tag ]
377 | [--created ]
378 | [--notebook ]
379 | [--reminder ]
380 | [--url ]
381 | ```
382 |
383 | ##### Options
384 |
385 | | Option | Argument | Description |
386 | |------------|----------|-------------|
387 | | ‑‑note | title of note which to edit | Tells Geeknote which note we want to edit. Geeknote searches by that name to locate a note. If Geeknote finds more than one note with such name, it will ask you to make a choice. |
388 | | ‑‑title | a new title | Use this option if you want to rename your note. Just set a new title, and Geeknote will rename the old one. |
389 | | ‑‑content | new content or "WRITE" | Enter the new content of your notes in text, or write instead the option "WRITE". In the first case the old content of the note will be replaced with the new content. In the second case Geeknote will get the current content and open it in Markdown in a text editor. |
390 | | ‑‑resource | attachment filename, like: document.pdf | Specify file to be attached to the note. May be repeated. Will replace existing resources. |
391 | | ‑‑tag | tag | Tag to be assigned to the note. May be repeated. Will replace existing tags. |
392 | | ‑‑created | date | Set note creation date date and time in either 'yyyy-mm-dd' or 'yyyy-mm-dd HH:MM' format. |
393 | | ‑‑notebook | target notebook | With this option you can change the notebook which contains your note. |
394 | | ‑‑reminder | date | Set reminder date and time in either 'yyyy-mm-dd' or 'yyyy-mm-dd HH:MM' format. Alternatively use TOMORROW and WEEK for 24 hours and a week ahead respectively, NONE for a reminder without a time. Use DONE to mark a reminder as completed. Use DELETE to remove reminder from a note. |
395 | | --urls | url | Set the URL for the note. |
396 | | --raw | | A flag signifying the content is in raw ENML format. |
397 | | --rawmd | | A flag signifying the content is in raw markdown format. |
398 |
399 | ##### Examples
400 |
401 | Renaming the note:
402 |
403 | ``` sh
404 | geeknote edit --note "Naughty List" --title "Nice List"
405 | ```
406 |
407 | Renaming the note and editing content in editor:
408 |
409 | ``` sh
410 | geeknote edit --note "Naughty List" --title "Nice List" --content "WRITE"
411 | ```
412 |
413 | ### Notes: Showing note content
414 |
415 | You can output any note in console using command *show* either independently or as a subsequent command to *find*. When you use *show* on a search made previously in which there was more than one result, Geeknote will ask you to make a choise.
416 |
417 | ##### Synopsis
418 |
419 | ``` sh
420 | geeknote show
421 | ```
422 |
423 | ##### Examples
424 |
425 | ``` sh
426 | $ geeknote show "Shop*"
427 |
428 | Total found: 2
429 | 1 : Grocery Shopping List
430 | 2 : Gift Shopping List
431 | 0 : -Cancel-
432 | : _
433 | ```
434 |
435 | As we mentioned before, *show* can use the results of previous search, so if you have already done the search, just call *show* with number of previous search results.
436 |
437 | ``` sh
438 | $ geeknote find --search "Shop*"
439 |
440 | Total found: 2
441 | 1 : Grocery Shopping List
442 | 2 : Gift Shopping List
443 |
444 | $ geeknote show 2
445 | ```
446 |
447 | ### Notes: Removing notes
448 | You can remove notes with Geeknotes from Evernote.
449 |
450 | ##### Synopsis
451 |
452 | ``` sh
453 | geeknote remove --note
454 | [--force]
455 | ```
456 |
457 | ##### Options
458 |
459 | | Option | Argument | Description |
460 | |--------------------|-----------------|-------------|
461 | | ‑‑note | note name | Name of the note you want to delete. If Geeknote will find more than one note, it will ask you to make a choice. |
462 | | ‑‑force | | A flag that says that Geeknote shouldn't ask for confirmation to remove note. |
463 |
464 | ##### Examples
465 |
466 | ``` sh
467 | geeknote remove --note "Shopping list"
468 | ```
469 |
470 | ### Notes: De-duplicating notes
471 | Geeknote can find and remove duplicate notes.
472 |
473 | ##### Synopsis
474 |
475 | ``` sh
476 | geeknote dedup [--notebook ]
477 | ```
478 |
479 | ##### Options
480 |
481 | | Option | Argument | Description |
482 | |--------------------|-----------------|-------------|
483 | | ‑‑notebook | notebook | Filter by notebook. |
484 |
485 | ##### Description
486 |
487 | Geeknote can locate notes that have the same title and content, and move duplicate notes to the trash.
488 | For large accounts, this process can take some time and might trigger the API rate limit.
489 | For that reason, it's possible to scope the de-duplication to a notebook at a time.
490 |
491 | ##### Examples
492 |
493 | ``` sh
494 | geeknote dedup --notebook Contacts
495 | ```
496 |
497 | ## Working with Notebooks
498 | ### Notebooks: show the list of notebooks
499 |
500 | Geeknote can display the list of all notebooks you have in Evernote.
501 |
502 | ##### Synopsis
503 |
504 | ``` sh
505 | geeknote notebook-list [--guid]
506 | ```
507 |
508 | ##### Options
509 |
510 | | Option | Argument | Description |
511 | |--------------------|-----------------|-------------|
512 | | ‑‑guid | | Show GUID of the notebook as substitute for result index. |
513 |
514 | ### Notebooks: creating a notebook
515 | With Geeknote you can create notebooks in Evernote right in console!
516 |
517 | ##### Synopsis
518 |
519 | ``` sh
520 | geeknote notebook-create --title
521 | ```
522 |
523 | ##### Options
524 |
525 | | Option | Argument | Description |
526 | |--------------------|-----------------|-------------|
527 | | ‑‑title | notebook title | With this option we specify the title of new note we want to create. |
528 |
529 | ##### Examples
530 |
531 | ``` sh
532 | geeknote notebook-create --title "Sport diets"
533 | ```
534 |
535 | ### Notebooks: renaming a notebook
536 |
537 | With Geeknote it's possible to rename existing notebooks in Evernote.
538 |
539 | ##### Synopsis
540 |
541 | ``` sh
542 | geeknote notebook-edit --notebook
543 | --title
544 | ```
545 |
546 | ##### Options
547 |
548 | | Option | Argument | Description |
549 | |--------------------|-----------------|-------------|
550 | | ‑‑notebook | old name | Name of existing notebook you want to rename. |
551 | | ‑‑title | new name | New title for notebook |
552 |
553 | ##### Examples
554 |
555 | ``` sh
556 | geeknote notebook-edit --notebook "Sport diets" --title "Hangover"
557 | ```
558 |
559 | ### Notebooks: removing a notebook
560 |
561 | With Geeknote it's possible to remove existing notebooks in Evernote.
562 |
563 | ##### Synopsis
564 |
565 | ``` sh
566 | geeknote notebook-remove --notebook
567 | [--force]
568 | ```
569 |
570 | ##### Options
571 |
572 | | Option | Argument | Description |
573 | |--------------------|-----------------|-------------|
574 | | ‑‑notebook | notebook | Name of existing notebook you want to delete. |
575 | | ‑‑force | | A flag that says that Geeknote shouldn't ask for confirmation to remove notebook. |
576 |
577 |
578 | ##### Examples
579 |
580 | ``` sh
581 | geeknote notebook-remove --notebook "Sport diets" --force
582 | ```
583 |
584 | ## Working with Tags
585 | ### Tags: showing the list of tags
586 |
587 | You can get the list of all tags you have in Evernote.
588 |
589 | ##### Synopsis
590 |
591 | ``` sh
592 | geeknote tag-list [--guid]
593 | ```
594 |
595 | ##### Options
596 |
597 | | Option | Argument | Description |
598 | |--------------------|-----------------|-------------|
599 | | ‑‑guid | | Show GUID of the tag as substitute for result index. |
600 |
601 | ### Tags: creating a new tag
602 | Usually tags are created with publishing new note. But if you need, you can create a new tag with Geeknote.
603 |
604 | ##### Synopsis
605 |
606 | ``` sh
607 | geeknote tag-create --title
608 | ```
609 |
610 | ##### Options
611 |
612 | | Option | Argument | Description |
613 | |--------------------|-----------------|-------------|
614 | | ‑‑title | tag name to create | Set the name of tag you want to create. |
615 |
616 | ##### Examples
617 |
618 | ``` sh
619 | geeknote tag-create --title "Hobby"
620 | ```
621 |
622 | ### Tags: renaming a tag
623 |
624 | You can rename the tag:
625 |
626 | ##### Synopsis
627 |
628 | ``` sh
629 | geeknote tag-edit --tagname
630 | --title
631 | ```
632 |
633 | ##### Options
634 |
635 | | Option | Argument | Description |
636 | |--------------------|-----------------|-------------|
637 | | ‑‑tagname | old name | Name of existing tag you want to rename. |
638 | | ‑‑title | new name | New name for tag. |
639 |
640 | ##### Examples
641 |
642 | ``` sh
643 | geeknote tag-edit --tagname "Hobby" --title "Girls"
644 | ```
645 |
646 | ## gnsync - synchronization app
647 |
648 | Gnsync is an additional application installed with Geeknote. Gnsync allows synchronization of files in local directories with Evernote. It works with text data and html with picture attachment support.
649 |
650 | ##### Synopsis
651 |
652 | ``` sh
653 | gnsync --path
654 | [--mask ]
655 | [--format ]
656 | [--notebook ]
657 | [--all]
658 | [--logpath ]
659 | [--two-way]
660 | [--download]
661 | ```
662 |
663 | ##### Options
664 |
665 | | Option | Argument | Description |
666 | |--------------------|-----------------|-------------|
667 | | ‑‑path | directory to sync | The directory you want to sync with Evernote. It should be the directory with text content files. |
668 | | ‑‑mask | unix shell-style wildcards to select the files | You can tell *gnsync* what filetypes to sync. By default *gnsync* tries to open every file in the directory. But you can set the mask like: *.txt, *.log, *.md, *.markdown. |
669 | | ‑‑format | in what format to save the note - plain or markdown | Set the engine which to use while files uploading. *gnsync* supports markdown and plain text formats. By default it uses plain text engine. |
670 | | ‑‑notebook | notebook where to save | You can set the notebook which will be syncronized with local directory. But if you won't set this option, *gnsync* will create new notebook with the name of the directory that you want to sync. |
671 | | ‑‑all | | You can specify to synchronize all notebooks already on the server, into subdirectories of the path. Useful with --download to do a backup of all notes. |
672 | | ‑‑logpath | path to logfile | *gnsync* can log information about syncing and with that option you can set the logfile. |
673 | | ‑‑two-way | | Normally *gnsync* will only upload files. Adding this flag will also make it download any notes not present as files in the notebook directory (after uploading any files not present as notes) |
674 | | ‑‑download-only | | Normally *gnsync* will only upload files. Adding this flag will make it download notes, but not upload any files |
675 |
676 | ##### Description
677 | The application *gnsync* is very useful in system administration, because you can syncronize you local logs, statuses and any other production information with Evernote.
678 |
679 | ##### Examples
680 |
681 | ``` sh
682 | gnsync --path /home/project/xmpp/logs/
683 | --mask "*.logs"
684 | --logpath /home/user/logs/xmpp2evernote.log
685 | --notebook "XMPP logs"
686 | ```
687 |
688 | ### Original Contributors
689 | * Vitaliy Rodnenko
690 | * Simon Moiseenko
691 | * Ivan Gureev
692 | * Roman Gladkov
693 | * Greg V
694 | * Ilya Shmygol
695 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | python:
3 | version: 2.7.11
4 |
5 | dependencies:
6 | pre:
7 | - pip install mock
8 | - pip install pytest
9 |
10 | override:
11 | - pip install .
12 |
13 | test:
14 | pre:
15 | - rm ./venv
16 | override:
17 | - py.test
18 |
--------------------------------------------------------------------------------
/completion/bash_completion/_geeknote:
--------------------------------------------------------------------------------
1 | _geeknote_command()
2 | {
3 | COMPREPLY=()
4 | cur="${COMP_WORDS[COMP_CWORD]}"
5 |
6 | SAVE_IFS=$IFS
7 | IFS=" "
8 | args="${COMP_WORDS[*]:1}"
9 | IFS=$SAVE_IFS
10 |
11 | COMPREPLY=( $(compgen -W "`geeknote autocomplete ${args}`" -- ${cur}) )
12 |
13 | return 0
14 | }
15 | complete -F _geeknote_command geeknote
16 |
--------------------------------------------------------------------------------
/completion/zsh_completion/_geeknote:
--------------------------------------------------------------------------------
1 | #compdef geeknote
2 | # --------------- ------------------------------------------------------------
3 | # Name : _geeknote
4 | # Synopsis : zsh completion for geeknote
5 | # Author : Zhao Cai
6 | # Date Created : Thu 11 Oct 2012 01:57:23 PM EDT
7 | # Last Modified : Thu 11 Oct 2012 02:25:47 PM EDT
8 | # Tag : [ shell, zsh, completion, evernote ]
9 | # Copyright : © 2012 by Zhao Cai,
10 | # Released under current GPL license.
11 | # --------------- ------------------------------------------------------------
12 | local ret
13 |
14 | ret=1
15 |
16 | geeknote_comp=( ${(z)"$(_call_program geeknote geeknote autocomplete $words[2,-1])"} )
17 | _describe 'geeknote' geeknote_comp && ret=0
18 |
19 | return ret
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/geeknote.rb:
--------------------------------------------------------------------------------
1 | class Geeknote < Formula
2 | include Language::Python::Virtualenv
3 |
4 | desc "Command-line client for Evernote"
5 | homepage 'https://github.com/jeffkowalski/geeknote'
6 | head 'https://github.com/jeffkowalski/geeknote.git'
7 |
8 | depends_on "python@2"
9 |
10 | resource "websocket-client" do
11 | url "https://files.pythonhosted.org/packages/a7/2b/0039154583cb0489c8e18313aa91ccd140ada103289c5c5d31d80fd6d186/websocket_client-0.40.0.tar.gz"
12 | sha256 "40ac14a0c54e14d22809a5c8d553de5a2ae45de3c60105fae53bcb281b3fe6fb"
13 | end
14 |
15 | resource "ipaddress" do
16 | url "https://files.pythonhosted.org/packages/4e/13/774faf38b445d0b3a844b65747175b2e0500164b7c28d78e34987a5bfe06/ipaddress-1.0.18.tar.gz"
17 | sha256 "5d8534c8e185f2d8a1fda1ef73f2c8f4b23264e8e30063feeb9511d492a413e1"
18 | end
19 |
20 | resource "orderedmultidict" do
21 | url "https://files.pythonhosted.org/packages/10/a5/a9596229782ffcb465f288588dff39ccd7f64fc453d64f75f5ef442315a8/orderedmultidict-0.7.11.tar.gz"
22 | sha256 "dc2320ca694d90dca4ecc8b9c5fdf71ca61d6c079d6feb085ef8d41585419a36"
23 | end
24 |
25 | resource "oauth2" do
26 | url "https://files.pythonhosted.org/packages/64/19/8b9066e94088e8d06d649e10319349bfca961e87768a525aba4a2627c986/oauth2-1.9.0.post1.tar.gz"
27 | sha256 "c006a85e7c60107c7cc6da1b184b5c719f6dd7202098196dfa6e55df669b59bf"
28 | end
29 |
30 | resource "httplib2" do
31 | url "https://files.pythonhosted.org/packages/e4/2e/a7e27d2c36076efeb8c0e519758968b20389adf57a9ce3af139891af2696/httplib2-0.10.3.tar.gz"
32 | sha256 "e404d3b7bd86c1bc931906098e7c1305d6a3a6dcef141b8bb1059903abb3ceeb"
33 | end
34 |
35 | resource "evernote" do
36 | url "https://files.pythonhosted.org/packages/3b/8e/dba34913e7dbccd868cdf228c5104f97ad97d4618994f0c5dd456496ae81/evernote-1.25.2.tar.gz"
37 | sha256 "69212c161e2538db13dd34e749125ff970f6c88aaa5f52f6925ffcf883107302"
38 | end
39 |
40 | resource "html2text" do
41 | url "https://files.pythonhosted.org/packages/22/c0/2d02a1fb9027f54796af2c2d38cf3a5b89319125b03734a9964e6db8dfa0/html2text-2016.9.19.tar.gz"
42 | sha256 "554ef5fd6c6cf6e3e4f725a62a3e9ec86a0e4d33cd0928136d1c79dbeb7b2d55"
43 | end
44 |
45 | resource "SQLAlchemy" do
46 | url "https://files.pythonhosted.org/packages/02/69/9473d60abef55445f8e967cfae215da5de29ca21b865c99d2bf02a45ee01/SQLAlchemy-1.1.9.tar.gz"
47 | sha256 "b65cdc73cd348448ef0164f6c77d45a9f27ca575d3c5d71ccc33adf684bc6ef0"
48 | end
49 |
50 | resource "markdown2" do
51 | url "https://files.pythonhosted.org/packages/2f/c0/6da6f0caec68c99dd4bd4661b0ac16a50bdad89b1bbeb5a40686826762dc/markdown2-2.3.3.zip"
52 | sha256 "20a9439a80d93c221080297119d9d31285a18c904489948deae3c7e05c7c5e38"
53 | end
54 |
55 | resource "beautifulsoup4" do
56 | url "https://files.pythonhosted.org/packages/9b/a5/c6fa2d08e6c671103f9508816588e0fb9cec40444e8e72993f3d4c325936/beautifulsoup4-4.5.3.tar.gz"
57 | sha256 "b21ca09366fa596043578fd4188b052b46634d22059e68dd0077d9ee77e08a3e"
58 | end
59 |
60 | resource "thrift" do
61 | url "https://files.pythonhosted.org/packages/a3/ea/84a41e03f1ab14fb314c8bcf1c451090efa14c5cdfb9797d1079f502b54e/thrift-0.10.0.zip"
62 | sha256 "b7f6c09155321169af03f9fb20dc15a4a0c7481e7c334a5ba8f7f0d864633209"
63 | end
64 |
65 | resource "future" do
66 | url "https://files.pythonhosted.org/packages/00/2b/8d082ddfed935f3608cc61140df6dcbf0edea1bc3ab52fb6c29ae3e81e85/future-0.16.0.tar.gz"
67 | sha256 "e39ced1ab767b5936646cedba8bcce582398233d6a627067d4c6a454c90cfedb"
68 | end
69 |
70 | # for proxyenv
71 | resource "docker-py" do
72 | url "https://files.pythonhosted.org/packages/fa/2d/906afc44a833901fc6fed1a89c228e5c88fbfc6bd2f3d2f0497fdfb9c525/docker-py-1.10.6.tar.gz"
73 | sha256 "4c2a75875764d38d67f87bc7d03f7443a3895704efc57962bdf6500b8d4bc415"
74 | end
75 |
76 | resource "htpasswd" do
77 | url "https://files.pythonhosted.org/packages/b9/2f/8b76f8b77125b75c3532966f3291f9e8787268be65fc4c9694887cba9375/htpasswd-2.3.tar.gz"
78 | sha256 "565f0b647a32549c663ccfddd1f501891daaf29242bbc6174bdd448120383e3d"
79 | end
80 |
81 | resource "proxyenv" do
82 | url "https://files.pythonhosted.org/packages/69/98/46baccf9ce353828726e0d302dad201e634fe4b50f6f61891f0721f40789/proxyenv-0.5.1.tar.gz"
83 | sha256 "e73caf8b063b7fbfb93b67e725a71469768262a9dddb4d9dfb79bb1e84cab4b9"
84 | end
85 |
86 | resource "lxml" do
87 | url "https://files.pythonhosted.org/packages/39/e8/a8e0b1fa65dd021d48fe21464f71783655f39a41f218293c1c590d54eb82/lxml-3.7.3.tar.gz"
88 | sha256 "aa502d78a51ee7d127b4824ff96500f0181d3c7826e6ee7b800d068be79361c7"
89 | end
90 |
91 | resource "backports.ssl_match_hostname" do
92 | url "https://files.pythonhosted.org/packages/76/21/2dc61178a2038a5cb35d14b61467c6ac632791ed05131dda72c20e7b9e23/backports.ssl_match_hostname-3.5.0.1.tar.gz"
93 | sha256 "502ad98707319f4a51fa2ca1c677bd659008d27ded9f6380c79e8932e38dcdf2"
94 | end
95 |
96 | resource "docker-pycreds" do
97 | url "https://files.pythonhosted.org/packages/95/2e/3c99b8707a397153bc78870eb140c580628d7897276960da25d8a83c4719/docker-pycreds-0.2.1.tar.gz"
98 | sha256 "93833a2cf280b7d8abbe1b8121530413250c6cd4ffed2c1cf085f335262f7348"
99 | end
100 |
101 | resource "requests" do
102 | url "https://files.pythonhosted.org/packages/16/09/37b69de7c924d318e51ece1c4ceb679bf93be9d05973bb30c35babd596e2/requests-2.13.0.tar.gz"
103 | sha256 "5722cd09762faa01276230270ff16af7acf7c5c45d623868d9ba116f15791ce8"
104 | end
105 |
106 | resource "six" do
107 | url "https://files.pythonhosted.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz"
108 | sha256 "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a"
109 | end
110 |
111 | def install
112 | virtualenv_install_with_resources
113 |
114 | bash_completion.install "completion/bash_completion/_geeknote" => "geeknote"
115 | zsh_completion.install "completion/zsh_completion/_geeknote" => "_geeknote"
116 | end
117 |
118 | test do
119 | system "py.test"
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/geeknote/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "3.1"
2 |
--------------------------------------------------------------------------------
/geeknote/argparser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .log import logging
4 | from . import out
5 |
6 |
7 | COMMANDS_DICT = {
8 | # User
9 | "user": {
10 | "help": "Show information about active user.",
11 | "flags": {
12 | "--full": {
13 | "help": "Show full information.",
14 | "value": True,
15 | "default": False,
16 | }
17 | },
18 | },
19 | "login": {"help": "Authorize in Evernote."},
20 | "logout": {
21 | "help": "Logout from Evernote.",
22 | "flags": {
23 | "--force": {
24 | "help": "Don't ask about logging out.",
25 | "value": True,
26 | "default": False,
27 | }
28 | },
29 | },
30 | "settings": {
31 | "help": "Show and edit current settings.",
32 | "arguments": {
33 | "--editor": {
34 | "help": "Set the editor, which use to " "edit and create notes.",
35 | "emptyValue": "#GET#",
36 | },
37 | "--note_ext": {
38 | "help": "Set default note's extension for markdown "
39 | "and raw formats."
40 | "\n Defaults to '.markdown, .org'",
41 | "emptyValue": "#GET#",
42 | },
43 | "--extras": {
44 | "help": "Set the markdown2 extra syntax, which use "
45 | "to convert markdown text to HTML. "
46 | "\n Please visit "
47 | "http://tinyurl.com/q459lur "
48 | "to get detail.",
49 | "emptyValue": "#GET#",
50 | },
51 | },
52 | },
53 | # Notes
54 | "create": {
55 | "help": "Create note in Evernote.",
56 | "arguments": {
57 | "--title": {"altName": "-t", "help": "The note title.", "required": True},
58 | "--content": {
59 | "altName": "-c",
60 | "help": "The note content.",
61 | "value": True,
62 | "default": "WRITE",
63 | },
64 | "--tag": {
65 | "altName": "-tg",
66 | "help": "Tag to be added to the note.",
67 | "repetitive": True,
68 | },
69 | "--created": {
70 | "altName": "-cr",
71 | "help": "Set local creation time in 'yyyy-mm-dd'"
72 | " or 'yyyy-mm-dd HH:MM' format.",
73 | },
74 | "--resource": {
75 | "altName": "-rs",
76 | "help": "Add a resource to the note.",
77 | "repetitive": True,
78 | },
79 | "--notebook": {
80 | "altName": "-nb",
81 | "help": "Set the notebook where to save note.",
82 | },
83 | "--reminder": {
84 | "altName": "-r",
85 | "help": "Set local reminder date and time in 'yyyy-mm-dd'"
86 | " or 'yyyy-mm-dd HH:MM' format."
87 | "\n Alternatively use TOMORROW "
88 | "and WEEK for 24 hours and a week ahead "
89 | "respectively,"
90 | "\n NONE for a reminder "
91 | "without a time. Use DONE to mark a "
92 | "reminder as completed.",
93 | },
94 | "--url": {"altname": "-u", "help": "Set the URL for the note."},
95 | },
96 | "flags": {
97 | "--raw": {
98 | "altName": "-r",
99 | "help": "Edit note with raw ENML",
100 | "value": True,
101 | "default": False,
102 | },
103 | "--rawmd": {
104 | "altName": "-rm",
105 | "help": "Edit note with raw markdown",
106 | "value": True,
107 | "default": False,
108 | },
109 | },
110 | },
111 | "create-linked": {
112 | "help": "Create Linked note in Evernote",
113 | "firstArg": "--notebook",
114 | "arguments": {
115 | "--title": {"altName": "-t", "help": "The note title.", "required": True},
116 | "--notebook": {
117 | "alt-name": "-nb",
118 | "help": "Name of the linked notebook in " "which to create this note.",
119 | "required": True,
120 | },
121 | },
122 | },
123 | "find": {
124 | "help": "Search notes in Evernote.",
125 | "firstArg": "--search",
126 | "arguments": {
127 | "--search": {"altName": "-s", "help": "Text to search.", "emptyValue": "*"},
128 | "--tag": {
129 | "altName": "-tg",
130 | "help": "Tag sought on the notes.",
131 | "repetitive": True,
132 | },
133 | "--notebook": {"altName": "-nb", "help": "Notebook containing the notes."},
134 | "--date": {
135 | "altName": "-d",
136 | "help": "Set date in 'yyyy-mm-dd' format or "
137 | "date range 'yyyy-mm-dd/yyyy-mm-dd' format.",
138 | },
139 | "--count": {
140 | "altName": "-cn",
141 | "help": "How many notes to show in the result list.",
142 | "type": int,
143 | },
144 | },
145 | "flags": {
146 | "--content-search": {
147 | "altName": "-cs",
148 | "help": "Search by content, not by title.",
149 | "value": True,
150 | "default": False,
151 | },
152 | "--exact-entry": {
153 | "altName": "-ee",
154 | "help": "Search for exact entry of the request.",
155 | "value": True,
156 | "default": False,
157 | },
158 | "--guid": {
159 | "altName": "-id",
160 | "help": "Replace ID with GUID of each note in results.",
161 | "value": True,
162 | "default": False,
163 | },
164 | "--ignore-completed": {
165 | "altName": "-C",
166 | "help": "Include only unfinished reminders",
167 | "value": True,
168 | "default": False,
169 | },
170 | "--reminders-only": {
171 | "altName": "-R",
172 | "help": "Include only notes with a reminder.",
173 | "value": True,
174 | "default": False,
175 | },
176 | "--deleted-only": {
177 | "altName": "-D",
178 | "help": "Include only notes that have been deleted.",
179 | "value": True,
180 | "default": False,
181 | },
182 | "--with-notebook": {
183 | "altName": "-wn",
184 | "help": "Add notebook of each note in results.",
185 | "value": True,
186 | "default": False,
187 | },
188 | "--with-tags": {
189 | "altName": "-wt",
190 | "help": "Add tag list of each note in results.",
191 | "value": True,
192 | "default": False,
193 | },
194 | "--with-url": {
195 | "altName": "-wu",
196 | "help": "Add direct url of each note "
197 | "in results to Evernote web-version.",
198 | "value": True,
199 | "default": False,
200 | },
201 | },
202 | },
203 | "edit": {
204 | "help": "Edit note in Evernote.",
205 | "firstArg": "--note",
206 | "arguments": {
207 | "--note": {
208 | "altName": "-n",
209 | "help": "The name or GUID or ID from the "
210 | "previous search of a note to edit.",
211 | "required": True,
212 | },
213 | "--title": {"altName": "-t", "help": "Set new title of the note."},
214 | "--content": {"altName": "-c", "help": "Set new content of the note."},
215 | "--resource": {
216 | "altName": "-rs",
217 | "help": "Add a resource to the note.",
218 | "repetitive": True,
219 | },
220 | "--tag": {
221 | "altName": "-tg",
222 | "help": "Set new tag for the note.",
223 | "repetitive": True,
224 | },
225 | "--created": {
226 | "altName": "-cr",
227 | "help": "Set local creation time in 'yyyy-mm-dd'"
228 | " or 'yyyy-mm-dd HH:MM' format.",
229 | },
230 | "--notebook": {
231 | "altName": "-nb",
232 | "help": "Assign new notebook for the note.",
233 | },
234 | "--reminder": {
235 | "altName": "-r",
236 | "help": "Set local reminder date and time in 'yyyy-mm-dd'"
237 | " or 'yyyy-mm-dd HH:MM' format."
238 | "\n Alternatively use TOMORROW "
239 | "and WEEK for 24 hours and a week ahead "
240 | "respectively,"
241 | "\n NONE for a reminder "
242 | "without a time. Use DONE to mark a "
243 | "reminder as completed."
244 | "\n Use DELETE to remove "
245 | "reminder from a note.",
246 | },
247 | "--url": {"altname": "-u", "help": "Set the URL for the note."},
248 | },
249 | "flags": {
250 | "--raw": {
251 | "altName": "-r",
252 | "help": "Edit note with raw ENML",
253 | "value": True,
254 | "default": False,
255 | },
256 | "--rawmd": {
257 | "altName": "-rm",
258 | "help": "Edit note with raw markdown",
259 | "value": True,
260 | "default": False,
261 | },
262 | },
263 | },
264 | "edit-linked": {
265 | "help": "Edit linked note in a shared notebook.",
266 | "firstArg": "--notebook",
267 | "arguments": {
268 | "--notebook": {
269 | "altName": "-nb",
270 | "help": "Name of the linked Notebook in which" "the note resides.",
271 | "required": True,
272 | },
273 | "--note": {
274 | "altName": "-n",
275 | "help": "Title of the Note you want to edit.",
276 | "required": True,
277 | },
278 | },
279 | },
280 | "show": {
281 | "help": "Output note in the terminal.",
282 | "firstArg": "--note",
283 | "arguments": {
284 | "--note": {
285 | "altName": "-n",
286 | "help": "The name or GUID or ID from the previous "
287 | "search of a note to show.",
288 | "required": True,
289 | }
290 | },
291 | "flags": {
292 | "--raw": {
293 | "altName": "-w",
294 | "help": "Show the raw note body",
295 | "value": True,
296 | "default": False,
297 | }
298 | },
299 | },
300 | "remove": {
301 | "help": "Remove note from Evernote.",
302 | "firstArg": "--note",
303 | "arguments": {
304 | "--note": {
305 | "altName": "-n",
306 | "help": "The name or GUID or ID from the previous "
307 | "search of a note to remove.",
308 | "required": True,
309 | }
310 | },
311 | "flags": {
312 | "--force": {
313 | "altName": "-f",
314 | "help": "Don't ask about removing.",
315 | "value": True,
316 | "default": False,
317 | }
318 | },
319 | },
320 | "dedup": {
321 | "help": "Find and remove duplicate notes in Evernote.",
322 | "arguments": {
323 | "--notebook": {
324 | "altName": "-nb",
325 | "help": "In which notebook search for duplicates.",
326 | }
327 | },
328 | },
329 | # Notebooks
330 | "notebook-list": {
331 | "help": "Show the list of existing notebooks in your Evernote.",
332 | "flags": {
333 | "--guid": {
334 | "altName": "-id",
335 | "help": "Replace ID with GUID " "of each notebook in results.",
336 | "value": True,
337 | "default": False,
338 | }
339 | },
340 | },
341 | "notebook-create": {
342 | "help": "Create new notebook.",
343 | "arguments": {
344 | "--title": {
345 | "altName": "-t",
346 | "help": "Set the title of new notebook.",
347 | "required": True,
348 | },
349 | "--stack": {"help": "Specify notebook stack container."},
350 | },
351 | },
352 | "notebook-edit": {
353 | "help": "Edit/rename notebook.",
354 | "firstArg": "--notebook",
355 | "arguments": {
356 | "--notebook": {
357 | "altName": "-nb",
358 | "help": "The name of a notebook to rename.",
359 | "required": True,
360 | },
361 | "--title": {"altName": "-t", "help": "Set the new name of notebook."},
362 | },
363 | },
364 | "notebook-remove": {
365 | "help": "Remove notebook.",
366 | "firstArg": "--notebook",
367 | "arguments": {
368 | "--notebook": {
369 | "altName": "-nb",
370 | "help": "The name of a notebook to remove.",
371 | "required": True,
372 | }
373 | },
374 | "flags": {
375 | "--force": {
376 | "help": "Don't ask about removing notebook.",
377 | "value": True,
378 | "default": False,
379 | }
380 | },
381 | },
382 | # Tags
383 | "tag-list": {
384 | "help": "Show the list of existing tags in your Evernote.",
385 | "flags": {
386 | "--guid": {
387 | "altName": "-id",
388 | "help": "Replace ID with GUID of each note in results.",
389 | "value": True,
390 | "default": False,
391 | }
392 | },
393 | },
394 | "tag-create": {
395 | "help": "Create new tag.",
396 | "arguments": {
397 | "--title": {
398 | "altName": "-t",
399 | "help": "Set the title of new tag.",
400 | "required": True,
401 | }
402 | },
403 | },
404 | "tag-edit": {
405 | "help": "Edit/rename tag.",
406 | "firstArg": "--tagname",
407 | "arguments": {
408 | "--tagname": {
409 | "altName": "-tgn",
410 | "help": "The name of a tag to rename.",
411 | "required": True,
412 | },
413 | "--title": {"altName": "-t", "help": "Set the new name of tag."},
414 | },
415 | },
416 | }
417 | """
418 | "tag-remove": {
419 | "help": "Remove tag.",
420 | "firstArg": "--tagname",
421 | "arguments": {
422 | "--tagname": {"help": "The name of a tag to remove.",
423 | "required": True},
424 | },
425 | "flags": {
426 | "--force": {"help": "Don't ask about removing.",
427 | "value": True,
428 | "default": False},
429 | }
430 | },
431 | """
432 |
433 |
434 | class argparser(object):
435 |
436 | COMMANDS = COMMANDS_DICT
437 | sys_argv = None
438 |
439 | def __init__(self, sys_argv):
440 | self.sys_argv = sys_argv
441 | self.LVL = len(sys_argv)
442 | self.INPUT = sys_argv
443 |
444 | # list of commands
445 | self.CMD_LIST = list(self.COMMANDS.keys())
446 | # command
447 | self.CMD = None if self.LVL == 0 else self.INPUT[0]
448 | # list of possible arguments of the command line
449 | self.CMD_ARGS = (
450 | self.COMMANDS[self.CMD]["arguments"]
451 | if self.LVL > 0
452 | and self.CMD in self.COMMANDS
453 | and "arguments" in self.COMMANDS[self.CMD]
454 | else {}
455 | )
456 | # list of possible flags of the command line
457 | self.CMD_FLAGS = (
458 | self.COMMANDS[self.CMD]["flags"]
459 | if self.LVL > 0
460 | and self.CMD in self.COMMANDS
461 | and "flags" in self.COMMANDS[self.CMD]
462 | else {}
463 | )
464 | # list of entered arguments and their values
465 | self.INP = [] if self.LVL <= 1 else self.INPUT[1:]
466 |
467 | logging.debug("CMD_LIST : %s", str(self.CMD_LIST))
468 | logging.debug("CMD: %s", str(self.CMD))
469 | logging.debug("CMD_ARGS : %s", str(self.CMD_ARGS))
470 | logging.debug("CMD_FLAGS : %s", str(self.CMD_FLAGS))
471 | logging.debug("INP : %s", str(self.INP))
472 |
473 | def parse(self):
474 | self.INP_DATA = {}
475 |
476 | if self.CMD is None:
477 | out.printAbout()
478 | return False
479 |
480 | if self.CMD == "autocomplete":
481 | # substitute arguments for AutoComplete
482 | # 1 offset to make the argument as 1 is autocomplete
483 | self.__init__(self.sys_argv[1:])
484 | self.printAutocomplete()
485 | return False
486 |
487 | if self.CMD == "--help":
488 | self.printHelp()
489 | return False
490 |
491 | if self.CMD not in self.COMMANDS:
492 | self.printErrorCommand()
493 | return False
494 |
495 | if "--help" in self.INP:
496 | self.printHelp()
497 | return False
498 |
499 | # prepare data
500 | for arg, params in list(self.CMD_ARGS.items()) + list(self.CMD_FLAGS.items()):
501 | # set values by default
502 | if "default" in params:
503 | self.INP_DATA[arg] = params["default"]
504 |
505 | # replace `altName` entered arguments on full
506 | if "altName" in params and params["altName"] in self.INP:
507 | self.INP[self.INP.index(params["altName"])] = arg
508 |
509 | activeArg = None
510 | ACTIVE_CMD = None
511 | # check and insert first argument by default
512 | if "firstArg" in self.COMMANDS[self.CMD]:
513 | firstArg = self.COMMANDS[self.CMD]["firstArg"]
514 | if len(self.INP) > 0:
515 | # Check that first argument is a default argument
516 | # and another argument.
517 | if self.INP[0] not in (list(self.CMD_ARGS.keys()) + list(self.CMD_FLAGS.keys())):
518 | self.INP = [firstArg] + self.INP
519 | else:
520 | self.INP = [firstArg]
521 |
522 | for item in self.INP:
523 | # check what are waiting the argument
524 | if activeArg is None:
525 | # actions for the argument
526 | if item in self.CMD_ARGS:
527 | activeArg = item
528 | ACTIVE_CMD = self.CMD_ARGS[activeArg]
529 |
530 | # actions for the flag
531 | elif item in self.CMD_FLAGS:
532 | self.INP_DATA[item] = self.CMD_FLAGS[item]["value"]
533 |
534 | # error. parameter is not found
535 | else:
536 | self.printErrorArgument(item)
537 | return False
538 |
539 | else:
540 | activeArgTmp = None
541 | # values it is parameter
542 | if item in self.CMD_ARGS or item in self.CMD_FLAGS:
543 | # active argument is "emptyValue"
544 | if "emptyValue" in ACTIVE_CMD:
545 | activeArgTmp = item # remember the new "active" argument
546 | item = ACTIVE_CMD[
547 | "emptyValue"
548 | ] # set the active argument to emptyValue
549 | # Error, "active" argument has no values
550 | else:
551 | self.printErrorArgument(activeArg, item)
552 | return False
553 |
554 | if "type" in ACTIVE_CMD:
555 | convType = ACTIVE_CMD["type"]
556 | if convType not in (int, str):
557 | logging.error("Unsupported argument type: %s", convType)
558 | return False
559 | try:
560 | item = convType(item)
561 | except:
562 | self.printErrorArgument(activeArg, item)
563 | return False
564 |
565 | if activeArg in self.INP_DATA:
566 | if "repetitive" in ACTIVE_CMD and (ACTIVE_CMD["repetitive"]):
567 | """ append """
568 | self.INP_DATA[activeArg] += [item]
569 | else:
570 | """ replace """
571 | self.INP_DATA[activeArg] = item
572 | else:
573 | """ set """
574 | if "repetitive" in ACTIVE_CMD and (ACTIVE_CMD["repetitive"]):
575 | self.INP_DATA[activeArg] = [item]
576 | else:
577 | self.INP_DATA[activeArg] = item
578 |
579 | activeArg = (
580 | activeArgTmp
581 | ) # this is either a new "active" argument or emptyValue.
582 |
583 | # if there are still active arguments
584 | if activeArg is not None:
585 | # if the active argument is emptyValue
586 | if "emptyValue" in ACTIVE_CMD:
587 | self.INP_DATA[activeArg] = ACTIVE_CMD["emptyValue"]
588 |
589 | # An error argument
590 | else:
591 | self.printErrorArgument(activeArg, "")
592 | return False
593 |
594 | # check whether there is a necessary argument request
595 | for arg, params in list(self.CMD_ARGS.items()) + list(self.CMD_FLAGS.items()):
596 | if "required" in params and arg not in self.INP:
597 | self.printErrorReqArgument(arg)
598 | return False
599 |
600 | # trim -- and ->_
601 | self.INP_DATA = dict(
602 | [key.lstrip("-").replace("-", "_"), val]
603 | for key, val in list(self.INP_DATA.items())
604 | )
605 | return self.INP_DATA
606 |
607 | def printAutocomplete(self):
608 | # checking later values
609 | LAST_VAL = self.INP[-1] if self.LVL > 1 else None
610 | PREV_LAST_VAL = self.INP[-2] if self.LVL > 2 else None
611 | ARGS_FLAGS_LIST = list(self.CMD_ARGS.keys()) + list(self.CMD_FLAGS.keys())
612 |
613 | # print root grid
614 | if self.CMD is None:
615 | self.printGrid(self.CMD_LIST)
616 |
617 | # work with root commands
618 | elif not self.INP:
619 |
620 | # print arguments if a root command is found
621 | if self.CMD in self.CMD_LIST:
622 | self.printGrid(ARGS_FLAGS_LIST)
623 |
624 | # autocomplete for sub-commands
625 | else:
626 | # filter out irrelevant commands
627 | self.printGrid(
628 | [item for item in self.CMD_LIST if item.startswith(self.CMD)]
629 | )
630 |
631 | # processing arguments
632 | else:
633 |
634 | # filter out arguments that have not been input
635 | if PREV_LAST_VAL in self.CMD_ARGS or LAST_VAL in self.CMD_FLAGS:
636 | self.printGrid(
637 | [item for item in ARGS_FLAGS_LIST if item not in self.INP]
638 | )
639 |
640 | # autocomplete for part of the command
641 | elif PREV_LAST_VAL not in self.CMD_ARGS:
642 | self.printGrid(
643 | [
644 | item
645 | for item in ARGS_FLAGS_LIST
646 | if item not in self.INP and item.startswith(LAST_VAL)
647 | ]
648 | )
649 |
650 | # processing of the arguments
651 | else:
652 | print("") # "Please_input_%s" % INP_ARG.replace('-', '')
653 |
654 | def printGrid(self, list):
655 | out.printLine(" ".join(list))
656 |
657 | def printErrorCommand(self):
658 | out.printLine('Unexpected command "%s"' % (self.CMD))
659 | self.printHelp()
660 |
661 | def printErrorReqArgument(self, errorArg):
662 | out.printLine(
663 | 'Not found required argument "%s" '
664 | 'for command "%s" ' % (errorArg, self.CMD)
665 | )
666 | self.printHelp()
667 |
668 | def printErrorArgument(self, errorArg, errorVal=None):
669 | if errorVal is None:
670 | out.printLine(
671 | 'Unexpected argument "%s" ' 'for command "%s"' % (errorArg, self.CMD)
672 | )
673 | else:
674 | out.printLine(
675 | 'Unexpected value "%s" ' 'for argument "%s"' % (errorVal, errorArg)
676 | )
677 | self.printHelp()
678 |
679 | def printHelp(self):
680 | if self.CMD is None or self.CMD not in self.COMMANDS:
681 | tab = len(max(list(self.COMMANDS.keys()), key=len))
682 | out.printLine("Available commands:")
683 | for cmd in sorted(self.COMMANDS):
684 | out.printLine(
685 | "%s : %s" % (cmd.rjust(tab, " "), self.COMMANDS[cmd]["help"])
686 | )
687 |
688 | else:
689 |
690 | tab = len(max(list(self.CMD_ARGS.keys()) + list(self.CMD_FLAGS.keys()), key=len))
691 |
692 | out.printLine("Options for: %s" % self.CMD)
693 | out.printLine("Available arguments:")
694 | for arg in self.CMD_ARGS:
695 | out.printLine(
696 | "%s : %s%s"
697 | % (
698 | arg.rjust(tab, " "),
699 | "[default] "
700 | if "firstArg" in self.COMMANDS[self.CMD]
701 | and self.COMMANDS[self.CMD]["firstArg"] == arg
702 | else "",
703 | self.CMD_ARGS[arg]["help"],
704 | )
705 | )
706 |
707 | if self.CMD_FLAGS:
708 | out.printLine("Available flags:")
709 | for flag in self.CMD_FLAGS:
710 | out.printLine(
711 | "%s : %s" % (flag.rjust(tab, " "), self.CMD_FLAGS[flag]["help"])
712 | )
713 |
--------------------------------------------------------------------------------
/geeknote/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import sys
5 |
6 | # Application path
7 | APP_DIR = os.path.join(os.getenv("HOME") or os.getenv("USERPROFILE"), ".geeknote")
8 | ERROR_LOG = os.path.join(APP_DIR, "error.log")
9 |
10 | ALWAYS_USE_YINXIANG = False # for 印象笔记 (Yìnxiàng bǐjì), set to True
11 |
12 | # !!! DO NOT EDIT !!! >>>
13 | if (
14 | ALWAYS_USE_YINXIANG
15 | or os.getenv("GEEKNOTE_BASE") == "yinxiang"
16 | or os.path.isfile(os.path.join(APP_DIR, "isyinxiang"))
17 | ):
18 | USER_BASE_URL = "app.yinxiang.com"
19 | else:
20 | USER_BASE_URL = "www.evernote.com"
21 |
22 | USER_STORE_URI = "https://{0}/edam/user".format(USER_BASE_URL)
23 |
24 | CONSUMER_KEY = "skaizer-5314"
25 | CONSUMER_SECRET = "6f4f9183b3120801"
26 |
27 | USER_BASE_URL_SANDBOX = "sandbox.evernote.com"
28 | USER_STORE_URI_SANDBOX = "https://sandbox.evernote.com/edam/user"
29 | CONSUMER_KEY_SANDBOX = "skaizer-1250"
30 | CONSUMER_SECRET_SANDBOX = "ed0fcc0c97c032a5"
31 | # !!! DO NOT EDIT !!! <<<
32 |
33 | # can be one of: UPDATED, CREATED, RELEVANCE, TITLE, UPDATE_SEQUENCE_NUMBER
34 | NOTE_SORT_ORDER = "UPDATED"
35 |
36 | # Evernote config
37 |
38 | try:
39 | IS_IN_TERMINAL = sys.stdin.isatty()
40 | IS_OUT_TERMINAL = sys.stdout.isatty()
41 | except:
42 | IS_IN_TERMINAL = False
43 | IS_OUT_TERMINAL = False
44 |
45 | # Set default system editor
46 | DEF_UNIX_EDITOR = "nano"
47 | DEF_WIN_EDITOR = "notepad.exe"
48 | EDITOR_OPEN = "WRITE"
49 |
50 | REMINDER_NONE = "NONE"
51 | REMINDER_DONE = "DONE"
52 | REMINDER_DELETE = "DELETE"
53 | # Shortcuts have a word and a number of seconds to add to the current time
54 | REMINDER_SHORTCUTS = {"TOMORROW": 86400000, "WEEK": 604800000}
55 |
56 | # Default file extensions for editing markdown and raw ENML, respectively
57 | DEF_NOTE_EXT = [".markdown", ".org"]
58 | # Accepted markdown extensions
59 | MARKDOWN_EXTENSIONS = [".md", ".markdown"]
60 | # Accepted html extensions
61 | HTML_EXTENSIONS = [".html", ".org"]
62 |
63 | DEV_MODE = False
64 | DEBUG = False
65 |
66 | # Url view the note via the web client
67 | NOTE_WEBCLIENT_URL = "https://%service%/Home.action?#n=%s"
68 | # Direct note link https://[service]/shard/[shardId]/nl/[userId]/[noteGuid]/ (see https://dev.evernote.com/doc/articles/note_links.php)
69 | NOTE_LINK = "https://%service%/shard/%s/nl/%s/%s"
70 |
71 | # Date format
72 | DEF_DATE_FORMAT = "%Y-%m-%d"
73 | DEF_DATE_AND_TIME_FORMAT = "%Y-%m-%d %H:%M"
74 | DEF_DATE_RANGE_DELIMITER = "/"
75 |
76 | if DEV_MODE:
77 | USER_STORE_URI = USER_STORE_URI_SANDBOX
78 | CONSUMER_KEY = CONSUMER_KEY_SANDBOX
79 | CONSUMER_SECRET = CONSUMER_SECRET_SANDBOX
80 | USER_BASE_URL = USER_BASE_URL_SANDBOX
81 | APP_DIR = os.path.join(
82 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config"
83 | )
84 | sys.stderr.write("Developer mode: using %s as application directory\n" % APP_DIR)
85 |
86 | ERROR_LOG = os.path.join(APP_DIR, "error.log")
87 |
88 | # validate config
89 | try:
90 | if not os.path.exists(APP_DIR):
91 | os.mkdir(APP_DIR)
92 | except Exception as e:
93 | sys.stdout.write("Cannot create application directory : %s" % APP_DIR)
94 | exit(1)
95 |
96 | if DEV_MODE:
97 | USER_STORE_URI = USER_STORE_URI_SANDBOX
98 | CONSUMER_KEY = CONSUMER_KEY_SANDBOX
99 | CONSUMER_SECRET = CONSUMER_SECRET_SANDBOX
100 | USER_BASE_URL = USER_BASE_URL_SANDBOX
101 |
102 | NOTE_WEBCLIENT_URL = NOTE_WEBCLIENT_URL.replace("%service%", USER_BASE_URL)
103 | NOTE_LINK = NOTE_LINK.replace("%service%", USER_BASE_URL)
104 |
--------------------------------------------------------------------------------
/geeknote/editor.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import sys
5 | import tempfile
6 | from bs4 import BeautifulSoup, NavigableString
7 | import threading
8 | import hashlib
9 | import html2text as html2text
10 | import markdown2 as markdown
11 | from . import tools
12 | from . import out
13 | import re
14 | from . import config
15 | from .storage import Storage
16 | from .log import logging
17 | from xml.sax.saxutils import escape, unescape
18 |
19 |
20 | class EditorThread(threading.Thread):
21 | def __init__(self, editor):
22 | threading.Thread.__init__(self)
23 | self.editor = editor
24 |
25 | def run(self):
26 | self.editor.edit()
27 |
28 |
29 | class Editor(object):
30 | # escape() and unescape() takes care of &, < and >.
31 |
32 | @staticmethod
33 | def getHtmlEscapeTable():
34 | return {'"': """, "'": "'", "\n": "
"}
35 |
36 | @staticmethod
37 | def getHtmlUnescapeTable():
38 | return dict((v, k) for k, v in list(Editor.getHtmlEscapeTable().items()))
39 |
40 | @staticmethod
41 | def HTMLEscape(text):
42 | return escape(text, Editor.getHtmlEscapeTable())
43 |
44 | @staticmethod
45 | def HTMLEscapeTag(text):
46 | return escape(text)
47 |
48 | @staticmethod
49 | def HTMLUnescape(text):
50 | return unescape(text, Editor.getHtmlUnescapeTable())
51 |
52 | @staticmethod
53 | def getImages(contentENML):
54 | """
55 | Creates a list of image resources to save.
56 | Each has a hash and extension attribute.
57 | """
58 | soup = BeautifulSoup(contentENML.decode("utf-8"), features='lxml')
59 | imageList = []
60 | for section in soup.findAll("en-media"):
61 | if "type" in section.attrs and "hash" in section.attrs:
62 | imageType, imageExtension = section["type"].split("/")
63 | if imageType == "image":
64 | imageList.append(
65 | {"hash": section["hash"], "extension": imageExtension}
66 | )
67 | return imageList
68 |
69 | @staticmethod
70 | def checklistInENMLtoSoup(soup):
71 | """
72 | Transforms Evernote checklist elements to github `* [ ]` task list style
73 | """
74 | for section in soup.findAll("en-todo", checked="true"):
75 | section.replace_with("
* [x]")
76 |
77 | for section in soup.findAll("en-todo"):
78 | section.replace_with("
* [ ]")
79 |
80 | @staticmethod
81 | def ENMLtoText(
82 | contentENML,
83 | format="default",
84 | imageOptions={"saveImages": False},
85 | imageFilename="",
86 | ):
87 | soup = BeautifulSoup(contentENML, "html.parser")
88 |
89 | if format == "pre":
90 | #
91 | # Expect to find at least one 'pre' section. Otherwise, the note
92 | # was not created using the format='pre' option. In that case,
93 | # revert back the defaults. When found, form the note from the
94 | # first 'pre' section only. The others were added by the user.
95 | #
96 | sections = soup.select("pre")
97 | if len(sections) >= 1:
98 | content = ""
99 | for c in sections[0].contents:
100 | content = "".join((content, c))
101 | pass
102 | else:
103 | format = "default"
104 |
105 | if format == "default":
106 | # In ENML, each line in paragraph have tag.
107 | for section in soup.find_all("div"):
108 | if not section.br:
109 | section.append(soup.new_tag("br"))
110 | section.unwrap()
111 |
112 | for section in soup.select("li > p"):
113 | section.replace_with(section.contents[0])
114 |
115 | for section in soup.select("li > br"):
116 | if section.next_sibling:
117 | next_sibling = section.next_sibling.next_sibling
118 | if next_sibling:
119 | if next_sibling.find("li"):
120 | section.extract()
121 | else:
122 | section.extract()
123 |
124 | Editor.checklistInENMLtoSoup(soup)
125 |
126 | # change
tags to
tags
127 | if "saveImages" in imageOptions and imageOptions["saveImages"]:
128 | for section in soup.findAll("en-media"):
129 | if "type" in section.attrs and "hash" in section.attrs:
130 | imageType, imageExtension = section["type"].split("/")
131 | if imageType == "image":
132 | newTag = soup.new_tag("img")
133 | newTag["src"] = "{}-{}.{}".format(
134 | imageOptions["baseFilename"],
135 | section["hash"],
136 | imageExtension,
137 | )
138 | section.replace_with(newTag)
139 |
140 | # Keep Evernote media elements in html format in markdown so
141 | # they'll stay in place over an edit
142 | for section in soup.find_all("en-media"):
143 | section.replace_with(str(section))
144 |
145 | content = html2text.html2text(str(soup), "")
146 |
147 | content = re.sub(r" *\n", os.linesep, content)
148 | content = content.replace(chr(160), " ") # no-break space
149 | content = Editor.HTMLUnescape(content)
150 |
151 | return content
152 |
153 | @staticmethod
154 | def wrapENML(contentHTML):
155 | body = (
156 | '\n'
157 | '\n'
158 | "%s" % contentHTML
159 | )
160 | return body
161 |
162 | @staticmethod
163 | def checklistInSoupToENML(soup):
164 | """
165 | Transforms github style checklists `* [ ]` in the BeautifulSoup tree to ENML.
166 | """
167 |
168 | checktodo_re = re.compile(r"\[([ x])\]")
169 |
170 | # To be more github compatible, if all elements in a list begin with '[ ]',
171 | # convert them to en-todo evernote elements
172 | for ul in soup.find_all("ul"):
173 | tasks = []
174 | istodo = True
175 |
176 | for li in ul.find_all("li"):
177 | task = soup.new_tag("div")
178 | todo_tag = soup.new_tag("en-todo")
179 |
180 | reg = checktodo_re.match(li.get_text())
181 | istodo = istodo and reg
182 | character = reg.group(1) if reg else None
183 | if character == "x":
184 | todo_tag["checked"] = "true"
185 |
186 | task.append(todo_tag)
187 | if reg:
188 | task.append(NavigableString(li.get_text()[3:].strip()))
189 | tasks.append(task)
190 |
191 | if istodo:
192 | for task in tasks[::-1]:
193 | ul.insert_after(task)
194 | ul.extract()
195 |
196 | @staticmethod
197 | def textToENML(content, raise_ex=False, format="markdown", rawmd=False):
198 | """
199 | Transform formatted text to ENML
200 | """
201 |
202 | if not isinstance(content, str):
203 | content = ""
204 | try:
205 | # add 2 space before new line in paragraph for creating br tags
206 | content = re.sub(r"([^\r\n])([\r\n])([^\r\n])", r"\1 \n\3", content)
207 | # content = re.sub(r'\r\n', '\n', content)
208 |
209 | if format == "pre":
210 | # For the 'pre' format, simply wrap the content with a 'pre' tag.
211 | # Do not perform any further parsing/mutation.
212 | contentHTML = "".join(("", content, "
")).encode("utf-8")
213 | elif format == "markdown":
214 | # Markdown format https://daringfireball.net/projects/markdown/basics
215 | extras = None
216 |
217 | if not rawmd:
218 | storage = Storage()
219 | extras = storage.getUserprop("markdown2_extras")
220 | content = Editor.HTMLEscapeTag(content)
221 |
222 | contentHTML = markdown.markdown(content, extras=extras)
223 |
224 | soup = BeautifulSoup(contentHTML, "html.parser")
225 | Editor.checklistInSoupToENML(soup)
226 | contentHTML = str(soup)
227 | elif format == "html":
228 | # Html to ENML http://dev.evernote.com/doc/articles/enml.php
229 | soup = BeautifulSoup(content, "html.parser")
230 | ATTR_2_REMOVE = [
231 | "id",
232 | "class",
233 | # "on*",
234 | "accesskey",
235 | "data",
236 | "dynsrc",
237 | "tabindex",
238 | ]
239 |
240 | for tag in soup.findAll():
241 | if hasattr(tag, "attrs"):
242 | for k in list(tag.attrs.keys()):
243 | if k in ATTR_2_REMOVE or k.find("on") == 0:
244 | tag.attrs.pop(k, None)
245 | contentHTML = str(soup)
246 | else:
247 | # Plain text format
248 | contentHTML = Editor.HTMLEscape(content)
249 |
250 | tmpstr = ""
251 | for l in contentHTML.split("\n"):
252 | if l == "":
253 | tmpstr = tmpstr + "
"
254 | else:
255 | tmpstr = tmpstr + "" + l + "
"
256 |
257 | contentHTML = tmpstr.encode("utf-8")
258 | contentHTML = contentHTML.replace(
259 | "[x]", ''
260 | )
261 | contentHTML = contentHTML.replace("[ ]", "")
262 |
263 | return Editor.wrapENML(contentHTML)
264 |
265 | except:
266 | import traceback
267 |
268 | traceback.print_exc()
269 | if raise_ex:
270 | raise Exception("Error while parsing text to html.")
271 | logging.error("Error while parsing text to html.")
272 | out.failureMessage("Error while parsing text to html.")
273 | return tools.exitErr()
274 |
275 | def __init__(self, editor, content, noteExtension, raw=False):
276 | if not isinstance(content, str):
277 | raise Exception(
278 | "Note content must be an instance "
279 | "of string, '%s' given." % type(content)
280 | )
281 |
282 | if not noteExtension:
283 | noteExtension = config.DEF_NOTE_EXT
284 | (tempfileHandler, tempfileName) = tempfile.mkstemp(suffix=noteExtension)
285 | content = content if raw else self.ENMLtoText(content)
286 | os.write(tempfileHandler, content.encode('utf-8'))
287 | os.close(tempfileHandler)
288 |
289 | self.content = content
290 | self.tempfile = tempfileName
291 | self.editor = editor
292 |
293 | def getTempfileChecksum(self):
294 | with open(self.tempfile, "rb") as fileHandler:
295 | checksum = hashlib.md5()
296 | while True:
297 | data = fileHandler.read(8192)
298 | if not data:
299 | break
300 | checksum.update(data)
301 |
302 | return checksum.hexdigest()
303 |
304 | def edit(self):
305 | """
306 | Call the system editor, that types as a default in the system.
307 | Editing goes in markdown format, and then the markdown
308 | converts into HTML, before uploading to Evernote.
309 | """
310 |
311 | # Try to find default editor in the system.
312 | editor = self.editor
313 | if not editor:
314 | editor = os.environ.get("editor")
315 |
316 | if not editor:
317 | editor = os.environ.get("EDITOR")
318 |
319 | if not editor:
320 | # If default editor is not finded, then use nano as a default.
321 | if sys.platform == "win32":
322 | editor = config.DEF_WIN_EDITOR
323 | else:
324 | editor = config.DEF_UNIX_EDITOR
325 |
326 | # Make a system call to open file for editing.
327 | logging.debug("launch system editor: %s %s" % (editor, self.tempfile))
328 |
329 | out.preloader.stop()
330 | os.system(editor + " " + self.tempfile)
331 | out.preloader.launch()
332 | newContent = open(self.tempfile, "r").read()
333 |
334 | return newContent
335 |
336 | def deleteTempfile(self):
337 | os.remove(self.tempfile)
338 |
--------------------------------------------------------------------------------
/geeknote/gclient.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | From old version API
5 | """
6 |
7 | from evernote.edam.error.ttypes import EDAMSystemException, EDAMUserException
8 | import evernote.edam.userstore.UserStore as UserStore
9 | import thrift.protocol.TBinaryProtocol as TBinaryProtocol
10 | from thrift.Thrift import TType, TMessageType
11 | from thrift.transport import TTransport
12 |
13 | try:
14 | from thrift.protocol import fastbinary
15 | except:
16 | fastbinary = None
17 |
18 |
19 | class getNoteStoreUrl_args(object):
20 | """
21 | Attributes:
22 | - authenticationToken
23 | """
24 |
25 | thrift_spec = (None, (1, TType.STRING, "authenticationToken", None, None))
26 |
27 | def __init__(self, authenticationToken=None):
28 | self.authenticationToken = authenticationToken
29 |
30 | def read(self, iprot):
31 | if (
32 | iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated
33 | and isinstance(iprot.trans, TTransport.CReadableTransport)
34 | and self.thrift_spec is not None
35 | and fastbinary is not None
36 | ):
37 | fastbinary.decode_binary(
38 | self, iprot.trans, (self.__class__, self.thrift_spec)
39 | )
40 | return
41 | iprot.readStructBegin()
42 | while True:
43 | (fname, ftype, fid) = iprot.readFieldBegin()
44 | if ftype == TType.STOP:
45 | break
46 | if fid == 1:
47 | if ftype == TType.STRING:
48 | self.authenticationToken = iprot.readString()
49 | else:
50 | iprot.skip(ftype)
51 | else:
52 | iprot.skip(ftype)
53 | iprot.readFieldEnd()
54 | iprot.readStructEnd()
55 |
56 | def write(self, oprot):
57 | if (
58 | oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated
59 | and self.thrift_spec is not None
60 | and fastbinary is not None
61 | ):
62 | oprot.trans.write(
63 | fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))
64 | )
65 | return
66 | oprot.writeStructBegin("getNoteStoreUrl_args")
67 | if self.authenticationToken is not None:
68 | oprot.writeFieldBegin("authenticationToken", TType.STRING, 1)
69 | oprot.writeString(self.authenticationToken)
70 | oprot.writeFieldEnd()
71 | oprot.writeFieldStop()
72 | oprot.writeStructEnd()
73 |
74 | def validate(self):
75 | return
76 |
77 | def __repr__(self):
78 | L = ["%s=%r" % (key, value) for key, value in self.__dict__.items()]
79 | return "%s(%s)" % (self.__class__.__name__, ", ".join(L))
80 |
81 | def __eq__(self, other):
82 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
83 |
84 | def __ne__(self, other):
85 | return not (self == other)
86 |
87 |
88 | class getNoteStoreUrl_result(object):
89 | """
90 | Attributes:
91 | - success
92 | - userException
93 | - systemException
94 | """
95 |
96 | thrift_spec = (
97 | (0, TType.STRING, "success", None, None),
98 | (
99 | 1,
100 | TType.STRUCT,
101 | "userException",
102 | (EDAMUserException, EDAMUserException.thrift_spec),
103 | None,
104 | ),
105 | (
106 | 2,
107 | TType.STRUCT,
108 | "systemException",
109 | (EDAMSystemException, EDAMSystemException.thrift_spec),
110 | None,
111 | ),
112 | )
113 |
114 | def __init__(self, success=None, userException=None, systemException=None):
115 | self.success = success
116 | self.userException = userException
117 | self.systemException = systemException
118 |
119 | def read(self, iprot):
120 | if (
121 | iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated
122 | and isinstance(iprot.trans, TTransport.CReadableTransport)
123 | and self.thrift_spec is not None
124 | and fastbinary is not None
125 | ):
126 | fastbinary.decode_binary(
127 | self, iprot.trans, (self.__class__, self.thrift_spec)
128 | )
129 | return
130 | iprot.readStructBegin()
131 | while True:
132 | (fname, ftype, fid) = iprot.readFieldBegin()
133 | if ftype == TType.STOP:
134 | break
135 | if fid == 0:
136 | if ftype == TType.STRING:
137 | self.success = iprot.readString()
138 | else:
139 | iprot.skip(ftype)
140 | elif fid == 1:
141 | if ftype == TType.STRUCT:
142 | self.userException = EDAMUserException()
143 | self.userException.read(iprot)
144 | else:
145 | iprot.skip(ftype)
146 | elif fid == 2:
147 | if ftype == TType.STRUCT:
148 | self.systemException = EDAMSystemException()
149 | self.systemException.read(iprot)
150 | else:
151 | iprot.skip(ftype)
152 | else:
153 | iprot.skip(ftype)
154 | iprot.readFieldEnd()
155 | iprot.readStructEnd()
156 |
157 | def write(self, oprot):
158 | if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated:
159 | if self.thrift_spec is not None and fastbinary is not None:
160 | oprot.trans.write(
161 | fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))
162 | )
163 | return
164 | oprot.writeStructBegin("getNoteStoreUrl_result")
165 | if self.success is not None:
166 | oprot.writeFieldBegin("success", TType.STRING, 0)
167 | oprot.writeString(self.success)
168 | oprot.writeFieldEnd()
169 | if self.userException is not None:
170 | oprot.writeFieldBegin("userException", TType.STRUCT, 1)
171 | self.userException.write(oprot)
172 | oprot.writeFieldEnd()
173 | if self.systemException is not None:
174 | oprot.writeFieldBegin("systemException", TType.STRUCT, 2)
175 | self.systemException.write(oprot)
176 | oprot.writeFieldEnd()
177 | oprot.writeFieldStop()
178 | oprot.writeStructEnd()
179 |
180 | def validate(self):
181 | return
182 |
183 | def __repr__(self):
184 | L = ["%s=%r" % (key, value) for key, value in self.__dict__.items()]
185 | return "%s(%s)" % (self.__class__.__name__, ", ".join(L))
186 |
187 | def __eq__(self, other):
188 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__
189 |
190 | def __ne__(self, other):
191 | return not (self == other)
192 |
193 |
194 | class CustomClient(UserStore.Client):
195 | """
196 | Getting from old version API
197 | """
198 |
199 | def getNoteStoreUrl(self, authenticationToken):
200 | """
201 | Returns the URL that should be used to talk to the NoteStore for the
202 | account represented by the provided authenticationToken.
203 | This method isn't needed by most clients, who can retrieve the correct
204 | NoteStore URL from the AuthenticationResult returned from the authenticate
205 | or refreshAuthentication calls. This method is typically only needed
206 | to look up the correct URL for a long-lived session token (e.g. for an
207 | OAuth web service).
208 |
209 | Parameters:
210 | - authenticationToken
211 | """
212 | self.send_getNoteStoreUrl(authenticationToken)
213 | return self.recv_getNoteStoreUrl()
214 |
215 | def send_getNoteStoreUrl(self, authenticationToken):
216 | self._oprot.writeMessageBegin("getNoteStoreUrl", TMessageType.CALL, self._seqid)
217 | args = getNoteStoreUrl_args()
218 | args.authenticationToken = authenticationToken
219 | args.write(self._oprot)
220 | self._oprot.writeMessageEnd()
221 | self._oprot.trans.flush()
222 |
223 | def recv_getNoteStoreUrl(self,):
224 | (fname, mtype, rseqid) = self._iprot.readMessageBegin()
225 | if mtype == TMessageType.EXCEPTION:
226 | x = UserStore.TApplicationException()
227 | x.read(self._iprot)
228 | self._iprot.readMessageEnd()
229 | raise x
230 | result = getNoteStoreUrl_result()
231 | result.read(self._iprot)
232 | self._iprot.readMessageEnd()
233 | if result.success is not None:
234 | return result.success
235 | if result.userException is not None:
236 | raise result.userException
237 | if result.systemException is not None:
238 | raise result.systemException
239 | raise UserStore.TApplicationException(
240 | UserStore.TApplicationException.MISSING_RESULT,
241 | "getNoteStoreUrl failed: unknown result",
242 | )
243 |
244 |
245 | GUserStore = UserStore
246 | GUserStore.Client = CustomClient
247 |
--------------------------------------------------------------------------------
/geeknote/gnsync.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import codecs
5 | import os
6 | import argparse
7 | import binascii
8 | import glob
9 | import logging
10 | import re
11 | import hashlib
12 | import binascii
13 | import mimetypes
14 |
15 | import evernote.edam.type.ttypes as Types
16 | from evernote.edam.limits.constants import EDAM_USER_NOTES_MAX
17 | from bs4 import BeautifulSoup
18 |
19 | from . import config
20 | from .geeknote import GeekNote
21 | from .storage import Storage
22 | from .editor import Editor
23 | from . import tools
24 |
25 | # for prototyping...
26 | # refactor should move code depending on these modules elsewhere
27 | import thrift.protocol.TBinaryProtocol as TBinaryProtocol
28 | import thrift.transport.THttpClient as THttpClient
29 | import urllib.parse
30 | import evernote.edam.notestore.NoteStore as NoteStore
31 |
32 | # set default logger (write log to file)
33 | def_logpath = os.path.join(config.APP_DIR, "gnsync.log")
34 | formatter = logging.Formatter("%(asctime)-15s : %(message)s")
35 | handler = logging.FileHandler(def_logpath)
36 | handler.setFormatter(formatter)
37 |
38 | logger = logging.getLogger(__name__)
39 | logger.setLevel(logging.DEBUG)
40 | logger.addHandler(handler)
41 |
42 | # http://en.wikipedia.org/wiki/Unicode_control_characters
43 | CONTROL_CHARS_RE = re.compile("[\x00-\x08\x0e-\x1f\x7f-\x9f]")
44 |
45 |
46 | def remove_control_characters(s):
47 | return CONTROL_CHARS_RE.sub("", s)
48 |
49 |
50 | def log(func):
51 | def wrapper(*args, **kwargs):
52 | try:
53 | return func(*args, **kwargs)
54 | except Exception as e:
55 | logger.exception("%s", str(e))
56 |
57 | return wrapper
58 |
59 |
60 | @log
61 | def reset_logpath(logpath):
62 | """
63 | Reset logpath to path from command line
64 | """
65 | global logger
66 |
67 | if not logpath:
68 | return
69 |
70 | # remove temporary log file if it's empty
71 | if os.path.isfile(def_logpath):
72 | if os.path.getsize(def_logpath) == 0:
73 | os.remove(def_logpath)
74 |
75 | # save previous handlers
76 | handlers = logger.handlers
77 |
78 | # remove old handlers
79 | for handler in handlers:
80 | logger.removeHandler(handler)
81 |
82 | # try to set new file handler
83 | handler = logging.FileHandler(logpath)
84 | handler.setFormatter(formatter)
85 | logger.addHandler(handler)
86 |
87 |
88 | def all_notebooks(sleep_on_ratelimit=False):
89 | geeknote = GeekNote(sleepOnRateLimit=sleep_on_ratelimit)
90 | return [notebook.name for notebook in geeknote.findNotebooks()]
91 |
92 |
93 | def all_linked_notebooks():
94 | geeknote = GeekNote()
95 | return geeknote.findLinkedNotebooks()
96 |
97 |
98 | class GNSync:
99 | notebook_name = None
100 | path = None
101 | mask = None
102 | twoway = None
103 | download_only = None
104 | nodownsync = None
105 |
106 | notebook_guid = None
107 | all_set = False
108 | sleep_on_ratelimit = False
109 |
110 | @log
111 | def __init__(
112 | self,
113 | notebook_name,
114 | path,
115 | mask,
116 | format,
117 | twoway=False,
118 | download_only=False,
119 | nodownsync=False,
120 | sleep_on_ratelimit=False,
121 | imageOptions={"saveImages": False, "imagesInSubdir": False},
122 | ):
123 | # check auth
124 | if not Storage().getUserToken():
125 | raise Exception("Auth error. There is not any oAuthToken.")
126 |
127 | # set path
128 | if not path:
129 | raise Exception("Path to sync directories does not select.")
130 |
131 | if not os.path.exists(path):
132 | raise Exception("Path to sync directories does not exist. %s" % path)
133 |
134 | self.path = path
135 |
136 | # set mask
137 | if not mask:
138 | mask = "*.*"
139 |
140 | self.mask = mask
141 |
142 | # set format
143 | if not format:
144 | format = "plain"
145 |
146 | self.format = format
147 |
148 | if format == "markdown":
149 | self.extension = ".md"
150 | elif format == "html":
151 | self.extension = ".html"
152 | else:
153 | self.extension = ".txt"
154 |
155 | self.twoway = twoway
156 | self.download_only = download_only
157 | self.nodownsync = nodownsync
158 |
159 | logger.info("Sync Start")
160 |
161 | # set notebook
162 | self.notebook_guid, self.notebook_name = self._get_notebook(notebook_name, path)
163 |
164 | # set image options
165 | self.imageOptions = imageOptions
166 |
167 | # all is Ok
168 | self.all_set = True
169 |
170 | self.sleep_on_ratelimit = sleep_on_ratelimit
171 |
172 | @log
173 | def sync(self):
174 | """
175 | Synchronize files to notes
176 | TODO: add two way sync with meta support
177 | TODO: add specific notebook support
178 | """
179 | if not self.all_set:
180 | return
181 |
182 | files = self._get_files()
183 | notes = self._get_notes()
184 |
185 | if not self.download_only:
186 | for f in files:
187 | has_note = False
188 | meta = self._parse_meta(self._get_file_content(f["path"]))
189 | title = f["name"] if "title" not in meta else meta["title"].strip()
190 | tags = (
191 | None
192 | if "tags" not in meta
193 | else meta["tags"].replace("[", "").replace("]", "").split(",")
194 | )
195 | tags = None if not tags else [x.strip() for x in tags]
196 | meta["tags"] = tags
197 | meta["title"] = title
198 | note = None
199 |
200 | if self.format == "html":
201 | meta["mtime"] = f["mtime"]
202 | note = self._html2note(meta)
203 |
204 | for n in notes:
205 | if title == n.title:
206 | has_note = True
207 | if f["mtime"] > n.updated:
208 | if self.format == "html":
209 | gn = GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit)
210 | note.guid = n.guid
211 | gn.getNoteStore().updateNote(gn.authToken, note)
212 | logger.info('Note "{0}" was updated'.format(note.title))
213 | else:
214 | self._update_note(f, n, title, meta["content"], tags)
215 | break
216 |
217 | if not has_note:
218 | if self.format == "html":
219 | gn = GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit)
220 | gn.getNoteStore().createNote(gn.authToken, note)
221 | logger.info('Note "{0}" was created'.format(note.title))
222 | else:
223 | self._create_note(f, title, meta["content"], tags)
224 |
225 | if self.twoway or self.download_only:
226 | for n in notes:
227 | has_file = False
228 | for f in files:
229 | if f["name"] == n.title:
230 | has_file = True
231 | if f["mtime"] < n.updated:
232 | self._update_file(f, n)
233 | break
234 |
235 | if not self.nodownsync:
236 | if not has_file:
237 | self._create_file(n)
238 |
239 | logger.info("Sync Complete")
240 |
241 | @log
242 | def _parse_meta(self, content):
243 | """
244 | Parse jekyll metadata of note, eg:
245 | ---
246 | layout: post
247 | title: draw uml with emacs
248 | tags: [uml, emacs]
249 | categories: [dev]
250 | ---
251 | and substitute meta from content.
252 |
253 | Caution: meta data will only work in one way
254 | mode! And I will never use two way mode, so
255 | two way sync will need your additional work!
256 | """
257 | metaBlock = re.compile("---(.*?)---", re.DOTALL)
258 | metaInfo = re.compile("(\w+):\s*?(.*)")
259 | block = metaBlock.search(content)
260 | if block is not None:
261 | info = metaInfo.findall(block.group(0))
262 | ret = dict(info)
263 | ret["content"] = metaBlock.sub("", content)
264 | return ret
265 | else:
266 | return {"content": content}
267 |
268 | @log
269 | def _html2note(self, meta):
270 | """
271 | parse html to note
272 | TODO: check if evernote need upload media evertime when update
273 | """
274 | note = Types.Note()
275 | note.title = meta["title"].strip() if "title" in meta else None
276 | note.tagNames = meta["tags"]
277 | note.created = meta["mtime"]
278 | note.resources = []
279 | soup = BeautifulSoup(meta["content"], "html.parser")
280 | for tag in soup.findAll("img"): # image support is enough
281 | if "src" in tag.attrs and len(tag.attrs["src"]) > 0:
282 | img = None
283 | with open(tag.attrs["src"], "rb") as f:
284 | img = f.read()
285 | md5 = hashlib.md5()
286 | md5.update(img)
287 | hash = md5.digest()
288 | hexHash = binascii.hexlify(hash)
289 | mime = mimetypes.guess_type(tag["src"])[0]
290 |
291 | data = Types.Data()
292 | data.size = len(img)
293 | data.bodyHash = hash
294 | data.body = img
295 |
296 | resource = Types.Resource()
297 | resource.mime = mime
298 | resource.data = data
299 |
300 | tag.name = "en-media"
301 | tag.attrs["type"] = mime
302 | tag.attrs["hash"] = hexHash
303 | tag.attrs.pop("src", None)
304 |
305 | note.resources.append(resource)
306 | note.notebookGuid = self.notebook_guid
307 | note.content = str(soup)
308 | return note
309 |
310 | @log
311 | def _update_note(self, file_note, note, title=None, content=None, tags=None):
312 | """
313 | Updates note from file
314 | """
315 | # content = self._get_file_content(file_note['path']) if content is None else content
316 | try:
317 | tags = tags or note.tagNames
318 | except AttributeError:
319 | tags = None
320 |
321 | result = GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).updateNote(
322 | guid=note.guid,
323 | title=title or note.title,
324 | content=content or self._get_file_content(file_note["path"]),
325 | tags=tags,
326 | notebook=self.notebook_guid,
327 | )
328 |
329 | if result:
330 | logger.info('Note "{0}" was updated'.format(note.title))
331 | else:
332 | raise Exception('Note "{0}" was not updated'.format(note.title))
333 |
334 | return result
335 |
336 | @log
337 | def _update_file(self, file_note, note):
338 | """
339 | Updates file from note
340 | """
341 | GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).loadNoteContent(note)
342 | content = Editor.ENMLtoText(note.content)
343 | open(file_note["path"], "w").write(content)
344 | updated_seconds = note.updated / 1000.0
345 | os.utime(file_note["path"], (updated_seconds, updated_seconds))
346 |
347 | @log
348 | def _create_note(self, file_note, title=None, content=None, tags=None):
349 | """
350 | Creates note from file
351 | """
352 |
353 | content = content or self._get_file_content(file_note["path"])
354 |
355 | if content is None:
356 | return
357 |
358 | result = GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).createNote(
359 | title=title or file_note["name"],
360 | content=content,
361 | notebook=self.notebook_guid,
362 | tags=tags or None,
363 | created=file_note["mtime"],
364 | )
365 |
366 | if result:
367 | logger.info('Note "{0}" was created'.format(title or file_note["name"]))
368 | else:
369 | raise Exception(
370 | 'Note "{0}" was not' " created".format(title or file_note["name"])
371 | )
372 |
373 | return result
374 |
375 | @log
376 | def _create_file(self, note):
377 | """
378 | Creates file from note
379 | """
380 | GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).loadNoteContent(note)
381 |
382 | escaped_title = re.sub(os.sep, "-", note.title)
383 |
384 | # Save images
385 | if "saveImages" in self.imageOptions and self.imageOptions["saveImages"]:
386 | imageList = Editor.getImages(note.content)
387 | if imageList:
388 | if (
389 | "imagesInSubdir" in self.imageOptions
390 | and self.imageOptions["imagesInSubdir"]
391 | ):
392 | try:
393 | os.mkdir(os.path.join(self.path, escaped_title + "_images"))
394 | except OSError:
395 | # Folder already exists
396 | pass
397 | imagePath = os.path.join(
398 | self.path, escaped_title + "_images", escaped_title
399 | )
400 | self.imageOptions["baseFilename"] = (
401 | escaped_title + "_images/" + escaped_title
402 | )
403 | else:
404 | imagePath = os.path.join(self.path, escaped_title)
405 | self.imageOptions["baseFilename"] = escaped_title
406 | for imageInfo in imageList:
407 | filename = "{}-{}.{}".format(
408 | imagePath, imageInfo["hash"], imageInfo["extension"]
409 | )
410 | logger.info("Saving image to {}".format(filename))
411 | binaryHash = binascii.unhexlify(imageInfo["hash"])
412 | if not GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).saveMedia(
413 | note.guid, binaryHash, filename
414 | ):
415 | logger.warning("Failed to save image {}".format(filename))
416 |
417 | content = Editor.ENMLtoText(note.content, self.imageOptions)
418 | path = os.path.join(self.path, escaped_title + self.extension)
419 | open(path, "w").write(content)
420 | updated_seconds = note.updated / 1000.0
421 | os.utime(path, (updated_seconds, updated_seconds))
422 | return True
423 |
424 | @log
425 | def _get_file_content(self, path):
426 | """
427 | Get file content.
428 | """
429 | with codecs.open(path, "r", encoding="utf-8") as f:
430 | content = f.read()
431 |
432 | # strip unprintable characters
433 | content = content.encode("ascii", errors="xmlcharrefreplace")
434 | content = Editor.textToENML(content=content, raise_ex=True, format=self.format)
435 |
436 | if content is None:
437 | logger.warning("File {0}. Content must be " "an UTF-8 encode.".format(path))
438 | return None
439 |
440 | return content
441 |
442 | @log
443 | def _get_notebook(self, notebook_name, path):
444 | """
445 | Get notebook guid and name.
446 | Takes default notebook if notebook's name does not select.
447 | """
448 | notebooks = GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit).findNotebooks()
449 |
450 | if not notebook_name:
451 | notebook_name = os.path.basename(os.path.realpath(path))
452 |
453 | notebook = [item for item in notebooks if item.name == notebook_name]
454 | guid = None
455 | if notebook:
456 | guid = notebook[0].guid
457 |
458 | if not guid:
459 | notebook = GeekNote(
460 | sleepOnRateLimit=self.sleep_on_ratelimit
461 | ).createNotebook(notebook_name)
462 |
463 | if notebook:
464 | logger.info('Notebook "{0}" was' " created".format(notebook_name))
465 | else:
466 | raise Exception(
467 | 'Notebook "{0}" was' " not created".format(notebook_name)
468 | )
469 |
470 | guid = notebook.guid
471 |
472 | return (guid, notebook_name)
473 |
474 | @log
475 | def _get_files(self):
476 | """
477 | Get files by self.mask from self.path dir.
478 | """
479 |
480 | file_paths = glob.glob(os.path.join(self.path, self.mask))
481 |
482 | files = []
483 | for f in file_paths:
484 | if os.path.isfile(f):
485 | file_name = os.path.basename(f)
486 | file_name = os.path.splitext(file_name)[0]
487 |
488 | mtime = int(os.path.getmtime(f) * 1000)
489 |
490 | files.append({"path": f, "name": file_name, "mtime": mtime})
491 |
492 | return files
493 |
494 | @log
495 | def _get_notes(self):
496 | """
497 | Get notes from evernote.
498 | """
499 | keywords = 'notebook:"{0}"'.format(
500 | tools.strip(self.notebook_name.encode("utf-8"))
501 | )
502 | return (
503 | GeekNote(sleepOnRateLimit=self.sleep_on_ratelimit)
504 | .findNotes(keywords, EDAM_USER_NOTES_MAX)
505 | .notes
506 | )
507 |
508 |
509 | def main():
510 | try:
511 | parser = argparse.ArgumentParser()
512 | parser.add_argument(
513 | "--path", "-p", action="store", help="Path to synchronize directory"
514 | )
515 | parser.add_argument(
516 | "--mask",
517 | "-m",
518 | action="store",
519 | help='Mask of files to synchronize. Default is "*.*"',
520 | )
521 | parser.add_argument(
522 | "--format",
523 | "-f",
524 | action="store",
525 | default="plain",
526 | choices=["plain", "markdown", "html"],
527 | help='The format of the file contents. Default is "plain". Valid values are "plain" "html" and "markdown"',
528 | )
529 | parser.add_argument(
530 | "--notebook",
531 | "-n",
532 | action="store",
533 | help="Notebook name for synchronize. Default is default notebook unless all is selected",
534 | )
535 | parser.add_argument(
536 | "--all",
537 | "-a",
538 | action="store_true",
539 | help="Synchronize all notebooks",
540 | default=False,
541 | )
542 | parser.add_argument(
543 | "--all-linked", action="store_true", help="Get all linked notebooks"
544 | )
545 | parser.add_argument(
546 | "--logpath",
547 | "-l",
548 | action="store",
549 | help="Path to log file. Default is GeekNoteSync in home dir",
550 | )
551 | parser.add_argument(
552 | "--two-way",
553 | "-t",
554 | action="store_true",
555 | help="Two-way sync (also download from evernote)",
556 | default=False,
557 | )
558 | parser.add_argument(
559 | "--download-only",
560 | action="store_true",
561 | help="Only download from evernote; no upload",
562 | default=False,
563 | )
564 | parser.add_argument(
565 | "--nodownsync",
566 | "-d",
567 | action="store",
568 | help="Sync from Evernote only if the file is already local.",
569 | )
570 | parser.add_argument(
571 | "--save-images", action="store_true", help="save images along with text"
572 | )
573 | parser.add_argument(
574 | "--sleep-on-ratelimit",
575 | action="store_true",
576 | help="sleep on being ratelimited",
577 | )
578 | parser.add_argument(
579 | "--images-in-subdir",
580 | action="store_true",
581 | help="save images in a subdirectory (instead of same directory as file)",
582 | )
583 |
584 | args = parser.parse_args()
585 |
586 | path = args.path if args.path else "."
587 | mask = args.mask if args.mask else None
588 | format = args.format if args.format else None
589 | notebook = args.notebook if args.notebook else None
590 | logpath = args.logpath if args.logpath else None
591 | twoway = args.two_way
592 | download_only = args.download_only
593 | nodownsync = True if args.nodownsync else False
594 |
595 | # image options
596 | imageOptions = {}
597 | imageOptions["saveImages"] = args.save_images
598 | imageOptions["imagesInSubdir"] = args.images_in_subdir
599 |
600 | reset_logpath(logpath)
601 |
602 | geeknote = GeekNote()
603 |
604 | if args.all_linked:
605 | my_map = {}
606 | for notebook in all_linked_notebooks():
607 | print("Syncing notebook: " + notebook.shareName)
608 | notebook_url = urllib.parse.urlparse(notebook.noteStoreUrl)
609 | sharedNoteStoreClient = THttpClient.THttpClient(notebook.noteStoreUrl)
610 | sharedNoteStoreProtocol = TBinaryProtocol.TBinaryProtocol(
611 | sharedNoteStoreClient
612 | )
613 | sharedNoteStore = NoteStore.Client(sharedNoteStoreProtocol)
614 |
615 | sharedAuthResult = sharedNoteStore.authenticateToSharedNotebook(
616 | notebook.shareKey, geeknote.authToken
617 | )
618 | sharedAuthToken = sharedAuthResult.authenticationToken
619 | sharedNotebook = sharedNoteStore.getSharedNotebookByAuth(
620 | sharedAuthToken
621 | )
622 |
623 | my_filter = NoteStore.NoteFilter(
624 | notebookGuid=sharedNotebook.notebookGuid
625 | )
626 |
627 | noteList = sharedNoteStore.findNotes(sharedAuthToken, my_filter, 0, 10)
628 |
629 | print("Found " + str(noteList.totalNotes) + " shared notes.")
630 |
631 | print(noteList.notes)
632 |
633 | filename = notebook.shareName + "-" + noteList.notes[0].title + ".html"
634 |
635 | filename = filename.replace(" ", "-").replace("/", "-")
636 |
637 | content = sharedNoteStore.getNoteContent(
638 | sharedAuthToken, noteList.notes[0].guid
639 | )
640 |
641 | with open(filename, "w") as f:
642 | f.write(content)
643 | return
644 |
645 | if args.all:
646 | for notebook in all_notebooks(sleep_on_ratelimit=args.sleep_on_ratelimit):
647 | logger.info("Syncing notebook %s", notebook)
648 | escaped_notebook_path = re.sub(os.sep, "-", notebook)
649 | notebook_path = os.path.join(path, escaped_notebook_path)
650 | if not os.path.exists(notebook_path):
651 | os.mkdir(notebook_path)
652 | GNS = GNSync(
653 | notebook,
654 | notebook_path,
655 | mask,
656 | format,
657 | twoway,
658 | download_only,
659 | nodownsync,
660 | sleep_on_ratelimit=args.sleep_on_ratelimit,
661 | imageOptions=imageOptions,
662 | )
663 | GNS.sync()
664 | else:
665 | GNS = GNSync(
666 | notebook,
667 | path,
668 | mask,
669 | format,
670 | twoway,
671 | download_only,
672 | nodownsync,
673 | sleep_on_ratelimit=args.sleep_on_ratelimit,
674 | imageOptions=imageOptions,
675 | )
676 | GNS.sync()
677 |
678 | except (KeyboardInterrupt, SystemExit, tools.ExitException):
679 | pass
680 |
681 | except Exception as e:
682 | logger.error(str(e))
683 |
684 |
685 | if __name__ == "__main__":
686 | main()
687 |
--------------------------------------------------------------------------------
/geeknote/log.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import logging
4 | from . import config
5 |
6 | if config.DEBUG:
7 | FORMAT = "%(filename)s %(funcName)s %(lineno)d : %(message)s"
8 | logging.basicConfig(format=FORMAT, level=logging.DEBUG)
9 | else:
10 | FORMAT = "%(asctime)-15s %(module)s %(funcName)s %(lineno)d : %(message)s"
11 | logging.basicConfig(format=FORMAT, filename=config.ERROR_LOG)
12 |
--------------------------------------------------------------------------------
/geeknote/oauth.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import http.client
4 | import time
5 | import http.cookies
6 | import uuid
7 | import re
8 | import base64
9 | from urllib.request import getproxies
10 | from urllib.parse import urlencode, unquote
11 | from urllib.parse import urlparse
12 | from lxml import html
13 |
14 | from . import out
15 | from . import tools
16 | from . import config
17 | from .log import logging
18 |
19 |
20 | class OAuthError(Exception):
21 | """Generic OAuth exception"""
22 |
23 |
24 | class GeekNoteAuth(object):
25 |
26 | consumerKey = config.CONSUMER_KEY
27 | consumerSecret = config.CONSUMER_SECRET
28 |
29 | url = {
30 | "base": config.USER_BASE_URL,
31 | "oauth": "/OAuth.action?oauth_token=%s",
32 | "access": "/OAuth.action",
33 | "token": "/oauth",
34 | "login": "/Login.action",
35 | "tfa": "/OTCAuth.action",
36 | }
37 |
38 | cookies = {}
39 |
40 | postData = {
41 | "login": {
42 | "login": "Sign in",
43 | "username": "",
44 | "password": "",
45 | "targetUrl": None,
46 | },
47 | "access": {
48 | "authorize": "Authorize",
49 | "oauth_token": None,
50 | "oauth_callback": None,
51 | "embed": "false",
52 | "expireMillis": "31536000000",
53 | },
54 | "tfa": {"code": "", "login": "Sign in"},
55 | }
56 |
57 | username = None
58 | password = None
59 | tmpOAuthToken = None
60 | verifierToken = None
61 | OAuthToken = None
62 | incorrectLogin = 0
63 | incorrectCode = 0
64 | code = None
65 |
66 | def __init__(self):
67 | try:
68 | proxy = getproxies()["https"]
69 | except KeyError:
70 | proxy = None
71 | if proxy is None:
72 | self._proxy = None
73 | else:
74 | # This assumes that the proxy is given in URL form.
75 | # A little simpler as _parse_proxy in urllib2.py
76 | self._proxy = urlparse(proxy)
77 |
78 | if proxy is None or not self._proxy.username:
79 | self._proxy_auth = None
80 | else:
81 | user_pass = "%s:%s" % (
82 | urlparse.unquote(self._proxy.username),
83 | urlparse.unquote(self._proxy.password),
84 | )
85 | self._proxy_auth = {
86 | "Proxy-Authorization": "Basic " + base64.b64encode(user_pass).strip()
87 | }
88 |
89 | def getTokenRequestData(self, **kwargs):
90 | params = {
91 | "oauth_consumer_key": self.consumerKey,
92 | "oauth_signature": self.consumerSecret + "%26",
93 | "oauth_signature_method": "PLAINTEXT",
94 | "oauth_timestamp": str(int(time.time())),
95 | "oauth_nonce": uuid.uuid4().hex,
96 | }
97 |
98 | if kwargs:
99 | params = dict(list(params.items()) + list(kwargs.items()))
100 |
101 | return params
102 |
103 | def loadPage(self, url, uri=None, method="GET", params="", additionalParams=""):
104 | if not url:
105 | logging.error("Request URL undefined")
106 | tools.exitErr()
107 |
108 | if not url.startswith("http"):
109 | url = "https://" + url
110 | urlData = urlparse(url)
111 | if not uri:
112 | url = "%s://%s"%(urlData.scheme, urlData.netloc)
113 | uri = urlData.path + "?" + urlData.query
114 |
115 | # prepare params, append to uri
116 | if params:
117 | params = urlencode(params) + additionalParams
118 | if method == "GET":
119 | uri += ("?" if uri.find("?") == -1 else "&") + params
120 | params = ""
121 |
122 | # insert local cookies in request
123 | headers = {
124 | "Cookie": "; ".join(
125 | [key + "=" + self.cookies[key] for key in list(self.cookies.keys())]
126 | )
127 | }
128 |
129 | if method == "POST":
130 | headers["Content-type"] = "application/x-www-form-urlencoded"
131 |
132 | if self._proxy is None:
133 | host = urlData.hostname
134 | port = urlData.port
135 | real_host = real_port = None
136 | else:
137 | host = self._proxy.hostname
138 | port = self._proxy.port
139 | real_host = urlData.hostname
140 | real_port = urlData.port
141 |
142 | logging.debug(
143 | "Request URL: %s:/%s > %s # %s",
144 | url,
145 | uri,
146 | unquote(params),
147 | headers["Cookie"],
148 | )
149 |
150 | conn = http.client.HTTPSConnection(host, port)
151 |
152 | if real_host is not None:
153 | conn.set_tunnel(real_host, real_port, headers=self._proxy_auth)
154 | if config.DEBUG:
155 | conn.set_debuglevel(1)
156 |
157 | conn.request(method, url + uri, params, headers)
158 | response = conn.getresponse()
159 | data = response.read()
160 | conn.close()
161 |
162 | logging.debug("Response : %s > %s", response.status, response.getheaders())
163 | result = tools.Struct(
164 | status=response.status,
165 | location=response.getheader("location", None),
166 | data=data,
167 | )
168 |
169 | # update local cookies
170 | sk = http.cookies.SimpleCookie(response.getheader("Set-Cookie", ""))
171 | for key in sk:
172 | self.cookies[key] = sk[key].value
173 | # delete cookies whose content is "deleteme"
174 | for key in list(self.cookies.keys()):
175 | if self.cookies[key] == "deleteme":
176 | del self.cookies[key]
177 |
178 | return result
179 |
180 | def parseResponse(self, data):
181 | data = unquote(data)
182 | return dict(item.split("=", 1) for item in data.split("?")[-1].split("&"))
183 |
184 | def getToken(self):
185 | # use developer token if set, otherwise authorize as usual
186 | import os
187 |
188 | token = os.environ.get("EVERNOTE_DEV_TOKEN")
189 | if token:
190 | return token
191 | else:
192 | out.preloader.setMessage("Authorize...")
193 | self.getTmpOAuthToken()
194 |
195 | self.login()
196 |
197 | out.preloader.setMessage("Allow Access...")
198 | self.allowAccess()
199 |
200 | out.preloader.setMessage("Getting Token...")
201 | self.getOAuthToken()
202 |
203 | # out.preloader.stop()
204 | return self.OAuthToken
205 |
206 | def getTmpOAuthToken(self):
207 | response = self.loadPage(
208 | self.url["base"],
209 | self.url["token"],
210 | "GET",
211 | self.getTokenRequestData(oauth_callback="https://" + self.url["base"]),
212 | )
213 |
214 | if response.status != 200:
215 | logging.error(
216 | "Unexpected response status on get " "temporary oauth_token 200 != %s",
217 | response.status,
218 | )
219 | raise OAuthError("OAuth token request failed")
220 |
221 | responseData = self.parseResponse(response.data)
222 | if "oauth_token" not in responseData:
223 | logging.error("OAuth temporary not found")
224 | raise OAuthError("OAuth token request failed")
225 |
226 | self.tmpOAuthToken = responseData["oauth_token"]
227 |
228 | logging.debug("Temporary OAuth token : %s", self.tmpOAuthToken)
229 |
230 | def handleTwoFactor(self):
231 | self.code = out.GetUserAuthCode()
232 | self.postData["tfa"]["code"] = self.code
233 | response = self.loadPage(
234 | self.url["base"],
235 | self.url["tfa"] + ";jsessionid=" + self.cookies["JSESSIONID"],
236 | "POST",
237 | self.postData["tfa"],
238 | )
239 | if not response.location and response.status == 200:
240 | if self.incorrectCode < 3:
241 | out.preloader.stop()
242 | out.printLine("Sorry, incorrect two factor code")
243 | out.preloader.setMessage("Authorize...")
244 | self.incorrectCode += 1
245 | return self.handleTwoFactor()
246 | else:
247 | logging.error("Incorrect two factor code")
248 |
249 | if not response.location:
250 | logging.error("Target URL was not found in the response on login")
251 | tools.exitErr()
252 |
253 | def login(self):
254 | response = self.loadPage(
255 | self.url["base"],
256 | self.url["login"],
257 | "GET",
258 | {"oauth_token": self.tmpOAuthToken},
259 | )
260 |
261 | # parse hpts and hptsh from page content
262 | hpts = re.search('.*\("hpts"\)\.value.*?"(.*?)"', response.data)
263 | hptsh = re.search('.*\("hptsh"\)\.value.*?"(.*?)"', response.data)
264 |
265 | if response.status != 200:
266 | logging.error(
267 | "Unexpected response status " "on login 200 != %s", response.status
268 | )
269 | tools.exitErr()
270 |
271 | if "JSESSIONID" not in self.cookies:
272 | logging.error("Not found value JSESSIONID in the response cookies")
273 | tools.exitErr()
274 |
275 | # get login/password
276 | self.username, self.password = out.GetUserCredentials()
277 |
278 | self.postData["login"]["username"] = self.username
279 | self.postData["login"]["password"] = self.password
280 | self.postData["login"]["targetUrl"] = self.url["oauth"] % self.tmpOAuthToken
281 | self.postData["login"]["hpts"] = hpts and hpts.group(1) or ""
282 | self.postData["login"]["hptsh"] = hptsh and hptsh.group(1) or ""
283 | response = self.loadPage(
284 | self.url["base"],
285 | self.url["login"] + ";jsessionid=" + self.cookies["JSESSIONID"],
286 | "POST",
287 | self.postData["login"],
288 | )
289 |
290 | if not response.location and response.status == 200:
291 | if self.incorrectLogin < 3:
292 | out.preloader.stop()
293 | out.printLine("Sorry, incorrect login or password")
294 | out.preloader.setMessage("Authorize...")
295 | self.incorrectLogin += 1
296 | return self.login()
297 | else:
298 | logging.error("Incorrect login or password")
299 |
300 | if not response.location:
301 | logging.error("Target URL was not found in the response on login")
302 | tools.exitErr()
303 |
304 | if response.status == 302:
305 | # the user has enabled two factor auth
306 | return self.handleTwoFactor()
307 |
308 | logging.debug("Success authorize, redirect to access page")
309 |
310 | # self.allowAccess(response.location)
311 |
312 | def allowAccess(self):
313 | response = self.loadPage(
314 | self.url["base"],
315 | self.url["access"],
316 | "GET",
317 | {"oauth_token": self.tmpOAuthToken},
318 | )
319 |
320 | logging.debug(response.data)
321 | tree = html.fromstring(response.data)
322 | token = (
323 | "&"
324 | + urlencode(
325 | {
326 | "csrfBusterToken": tree.xpath(
327 | "//input[@name='csrfBusterToken']/@value"
328 | )[0]
329 | }
330 | )
331 | + "&"
332 | + urlencode(
333 | {
334 | "csrfBusterToken": tree.xpath(
335 | "//input[@name='csrfBusterToken']/@value"
336 | )[1]
337 | }
338 | )
339 | )
340 | sourcePage = tree.xpath("//input[@name='_sourcePage']/@value")[0]
341 | fp = tree.xpath("//input[@name='__fp']/@value")[0]
342 | targetUrl = tree.xpath("//input[@name='targetUrl']/@value")[0]
343 | logging.debug(token)
344 |
345 | if response.status != 200:
346 | logging.error(
347 | "Unexpected response status " "on login 200 != %s", response.status
348 | )
349 | tools.exitErr()
350 |
351 | if "JSESSIONID" not in self.cookies:
352 | logging.error("Not found value JSESSIONID in the response cookies")
353 | tools.exitErr()
354 |
355 | access = self.postData["access"]
356 | access["oauth_token"] = self.tmpOAuthToken
357 | access["oauth_callback"] = ""
358 | access["embed"] = "false"
359 | access["suggestedNotebookName"] = "Geeknote"
360 | access["supportLinkedSandbox"] = ""
361 | access["analyticsLoginOrigin"] = "Other"
362 | access["clipperFlow"] = "false"
363 | access["showSwitchService"] = "true"
364 | access["_sourcePage"] = sourcePage
365 | access["__fp"] = fp
366 | access["targetUrl"] = targetUrl
367 |
368 | response = self.loadPage(
369 | self.url["base"], self.url["access"], "POST", access, token
370 | )
371 |
372 | if response.status != 302:
373 | logging.error(
374 | "Unexpected response status on allowing " "access 302 != %s",
375 | response.status,
376 | )
377 | logging.error(response.data)
378 | tools.exitErr()
379 |
380 | responseData = self.parseResponse(response.location)
381 | if "oauth_verifier" not in responseData:
382 | logging.error("OAuth verifier not found")
383 | tools.exitErr()
384 |
385 | self.verifierToken = responseData["oauth_verifier"]
386 |
387 | logging.debug("OAuth verifier token take")
388 |
389 | # self.getOAuthToken(verifier)
390 |
391 | def getOAuthToken(self):
392 | response = self.loadPage(
393 | self.url["base"],
394 | self.url["token"],
395 | "GET",
396 | self.getTokenRequestData(
397 | oauth_token=self.tmpOAuthToken, oauth_verifier=self.verifierToken
398 | ),
399 | )
400 |
401 | if response.status != 200:
402 | logging.error(
403 | "Unexpected response status on " "getting oauth token 200 != %s",
404 | response.status,
405 | )
406 | tools.exitErr()
407 |
408 | responseData = self.parseResponse(response.data)
409 | if "oauth_token" not in responseData:
410 | logging.error("OAuth token not found")
411 | tools.exitErr()
412 |
413 | logging.debug("OAuth token take : %s", responseData["oauth_token"])
414 | self.OAuthToken = responseData["oauth_token"]
415 |
--------------------------------------------------------------------------------
/geeknote/out.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import getpass
4 | import threading
5 | import _thread
6 | import time
7 | import datetime
8 | import sys
9 | import os.path
10 |
11 | from .__init__ import __version__
12 | from . import tools
13 | from . import config
14 |
15 |
16 | def preloaderPause(fn, *args, **kwargs):
17 | def wrapped(*args, **kwargs):
18 | if not preloader.isLaunch:
19 | return fn(*args, **kwargs)
20 |
21 | preloader.stop()
22 | result = fn(*args, **kwargs)
23 | preloader.launch()
24 |
25 | return result
26 |
27 | return wrapped
28 |
29 |
30 | def preloaderStop(fn, *args, **kwargs):
31 | def wrapped(*args, **kwargs):
32 | if not preloader.isLaunch:
33 | return fn(*args, **kwargs)
34 |
35 | preloader.stop()
36 | result = fn(*args, **kwargs)
37 | return result
38 |
39 | return wrapped
40 |
41 |
42 | class preloader(object):
43 | progress = ("> ", ">> ", ">>>", " >>", " >", " ")
44 | clearLine = "\r" + " " * 40 + "\r"
45 | message = None
46 | isLaunch = False
47 | counter = 0
48 |
49 | @staticmethod
50 | def setMessage(message, needLaunch=True):
51 | preloader.message = message
52 | if not preloader.isLaunch and needLaunch:
53 | preloader.launch()
54 |
55 | @staticmethod
56 | def launch():
57 | if not config.IS_OUT_TERMINAL:
58 | return
59 | preloader.counter = 0
60 | preloader.isLaunch = True
61 | _thread.start_new_thread(preloader.draw, ())
62 |
63 | @staticmethod
64 | def stop():
65 | if not config.IS_OUT_TERMINAL:
66 | return
67 | preloader.counter = -1
68 | printLine(preloader.clearLine, "")
69 | preloader.isLaunch = False
70 |
71 | @staticmethod
72 | def exit(code=0):
73 | preloader.stop()
74 |
75 | if threading.current_thread().__class__.__name__ == "_MainThread":
76 | sys.exit(code)
77 | else:
78 | _thread.exit()
79 |
80 | @staticmethod
81 | def draw():
82 | try:
83 | if not preloader.isLaunch:
84 | return
85 |
86 | while preloader.counter >= 0:
87 | printLine(preloader.clearLine, "")
88 | preloader.counter += 1
89 | printLine(
90 | "%s : %s"
91 | % (
92 | preloader.progress[preloader.counter % len(preloader.progress)],
93 | preloader.message,
94 | ),
95 | "",
96 | )
97 |
98 | time.sleep(0.3)
99 | except:
100 | pass
101 |
102 |
103 | def _getCredentialsFromFile():
104 | # Get evernote credentials from file APP_DIR/credentials
105 | # This is used only for sandbox mode (DEV_MODE=True) for security reasons
106 | if config.DEV_MODE:
107 | creds = os.path.join(config.APP_DIR, "credentials")
108 | if os.path.exists(creds):
109 | credentials = None
110 | # execfile doesn't work reliably for assignments, see python docs
111 | with open(creds, "r") as f:
112 | # this sets "credentials" if correctly formatted
113 | exec(f.read())
114 | try:
115 | return credentials.split(":")
116 | except:
117 | sys.stderr.write(
118 | """Error reading credentials from %s.
119 | Format should be:
120 | credentials="::"
121 |
122 | """
123 | % creds
124 | )
125 | return None
126 |
127 |
128 | @preloaderPause
129 | def GetUserCredentials():
130 | """Prompts the user for a username and password."""
131 | creds = _getCredentialsFromFile()
132 | if creds is not None:
133 | return creds[:2]
134 |
135 | try:
136 | login = None
137 | password = None
138 | if login is None:
139 | login = rawInput("Login: ")
140 |
141 | if password is None:
142 | password = rawInput("Password: ", True)
143 | except (KeyboardInterrupt, SystemExit) as e:
144 | if e.message:
145 | tools.exit(e.message)
146 | else:
147 | tools.exit
148 |
149 | return (login, password)
150 |
151 |
152 | @preloaderPause
153 | def GetUserAuthCode():
154 | """Prompts the user for a two factor auth code."""
155 | creds = _getCredentialsFromFile()
156 | if creds is not None:
157 | return creds[2]
158 |
159 | try:
160 | code = None
161 | if code is None:
162 | code = rawInput("Two-Factor Authentication Code: ")
163 | except (KeyboardInterrupt, SystemExit) as e:
164 | if e.message:
165 | tools.exit(e.message)
166 | else:
167 | tools.exit
168 |
169 | return code
170 |
171 |
172 | @preloaderStop
173 | def SearchResult(listItems, request, **kwargs):
174 | """Print search results."""
175 | printLine("Search request: %s" % request)
176 | printList(listItems, **kwargs)
177 |
178 |
179 | @preloaderStop
180 | def SelectSearchResult(listItems, **kwargs):
181 | """Select a search result."""
182 | return printList(listItems, showSelector=True, **kwargs)
183 |
184 |
185 | @preloaderStop
186 | def confirm(message):
187 | printLine(message)
188 | try:
189 | while True:
190 | answer = rawInput("Yes/No: ")
191 | if answer.lower() in ["yes", "ye", "y"]:
192 | return True
193 | if answer.lower() in ["no", "n"]:
194 | return False
195 | failureMessage('Incorrect answer "%s", ' "please try again:\n" % answer)
196 | except (KeyboardInterrupt, SystemExit) as e:
197 | if e.message:
198 | tools.exit(e.message)
199 | else:
200 | tools.exit
201 |
202 |
203 | @preloaderStop
204 | def showNote(note, id, shardId):
205 | separator("#", "URL")
206 | printLine("NoteLink: " + (config.NOTE_LINK % (shardId, id, note.guid)))
207 | printLine("WebClientURL: " + (config.NOTE_WEBCLIENT_URL % note.guid))
208 | separator("#", "TITLE")
209 | printLine(note.title)
210 | separator("=", "META")
211 | printLine("Notebook: %s" % note.notebookName)
212 | printLine("Created: %s" % printDate(note.created))
213 | printLine("Updated: %s" % printDate(note.updated))
214 | for key, value in list(note.attributes.__dict__.items()):
215 | if value and key not in ("reminderOrder", "reminderTime", "reminderDoneTime"):
216 | printLine("%s: %s" % (key, value))
217 | separator("|", "REMINDERS")
218 | printLine("Order: %s" % str(note.attributes.reminderOrder))
219 | printLine("Time: %s" % printDate(note.attributes.reminderTime))
220 | printLine("Done: %s" % printDate(note.attributes.reminderDoneTime))
221 | separator("-", "CONTENT")
222 | if note.tagNames:
223 | printLine("Tags: %s" % ", ".join(note.tagNames))
224 |
225 | from .editor import Editor
226 |
227 | printLine(Editor.ENMLtoText(note.content))
228 |
229 |
230 | @preloaderStop
231 | def showNoteRaw(note):
232 | from .editor import Editor
233 |
234 | printLine(Editor.ENMLtoText(note.content, "pre"))
235 |
236 |
237 | @preloaderStop
238 | def showUser(user, fullInfo):
239 | separator("#", "USER INFO")
240 | colWidth = 17
241 | printLine("%s: %s" % ("Username".ljust(colWidth, " "), user.username))
242 | printLine("%s: %s" % ("Name".ljust(colWidth, " "), user.name))
243 | printLine("%s: %s" % ("Email".ljust(colWidth, " "), user.email))
244 |
245 | if fullInfo:
246 | printLine(
247 | "%s: %.2f MB"
248 | % (
249 | "Upload limit".ljust(colWidth, " "),
250 | (int(user.accounting.uploadLimit) / 1024 / 1024),
251 | )
252 | )
253 | printLine(
254 | "%s: %s"
255 | % (
256 | "Upload limit end".ljust(colWidth, " "),
257 | printDate(user.accounting.uploadLimitEnd),
258 | )
259 | )
260 | printLine("%s: %s" % ("Timezone".ljust(colWidth, " "), user.timezone))
261 |
262 |
263 | @preloaderStop
264 | def successMessage(message):
265 | """ Displaying a message. """
266 | printLine(message, "\n")
267 |
268 |
269 | @preloaderStop
270 | def failureMessage(message):
271 | """ Displaying a message."""
272 | printLine(message, "\n", sys.stderr)
273 |
274 |
275 | def separator(symbol="", title=""):
276 | size = 40
277 | if title:
278 | sw = int((size - len(title) + 2) / 2)
279 | printLine(
280 | "%s %s %s" % (symbol * sw, title, symbol * (sw - (len(title) + 1) % 2))
281 | )
282 |
283 | else:
284 | printLine(symbol * size + "\n")
285 |
286 |
287 | @preloaderStop
288 | def printList(
289 | listItems,
290 | title="",
291 | showSelector=False,
292 | showByStep=0,
293 | showUrl=False,
294 | showTags=False,
295 | showNotebook=False,
296 | showGUID=False,
297 | ):
298 |
299 | if title:
300 | separator("=", title)
301 |
302 | total = len(listItems)
303 | printLine("Found %d item%s" % (total, ("s" if total != 1 else "")))
304 | for key, item in enumerate(listItems):
305 | key += 1
306 |
307 | printLine(
308 | "%s : %s%s%s%s%s%s"
309 | % (
310 | item.guid
311 | if showGUID and hasattr(item, "guid")
312 | else str(key).rjust(3, " "),
313 | printDate(item.created).ljust(11, " ")
314 | if hasattr(item, "created")
315 | else "",
316 | printDate(item.updated).ljust(11, " ")
317 | if hasattr(item, "updated")
318 | else "",
319 | item.notebookName.ljust(18, " ")
320 | if showNotebook and hasattr(item, "notebookName")
321 | else "",
322 | item.title
323 | if hasattr(item, "title")
324 | else item.name
325 | if hasattr(item, "name")
326 | else item.shareName,
327 | "".join([" #" + s for s in item.tagGuids])
328 | if showTags and hasattr(item, "tagGuids") and item.tagGuids
329 | else "",
330 | " " + (">>> " + config.NOTE_WEBCLIENT_URL % item.guid)
331 | if showUrl
332 | else "",
333 | )
334 | )
335 |
336 | if showByStep != 0 and key % showByStep == 0 and key < total:
337 | printLine("-- More --", "\r")
338 | tools.getch()
339 | printLine(" " * 12, "\r")
340 |
341 | if showSelector:
342 | printLine(" 0 : -Cancel-")
343 | try:
344 | while True:
345 | num = rawInput(": ")
346 | if tools.checkIsInt(num) and 1 <= int(num) <= total:
347 | return listItems[int(num) - 1]
348 | if num == "0" or num == "q":
349 | exit(1)
350 | failureMessage('Incorrect number "%s", ' "please try again:\n" % num)
351 | except (KeyboardInterrupt, SystemExit) as e:
352 | if e.message:
353 | tools.exit(e.message)
354 | else:
355 | tools.exit
356 |
357 |
358 | def rawInput(message, isPass=False):
359 | if isPass:
360 | data = getpass.getpass(message)
361 | else:
362 | data = input(message)
363 | return tools.stdinEncode(data)
364 |
365 |
366 | # return a timezone-localized formatted representation of a UTC timestamp
367 | def printDate(timestamp):
368 | if timestamp is None:
369 | return "None"
370 | else:
371 | return datetime.datetime.fromtimestamp(timestamp / 1000).strftime(
372 | config.DEF_DATE_FORMAT
373 | )
374 |
375 |
376 | def printLine(line, endLine="\n", out=None):
377 | # "out = sys.stdout" makes it hard to mock
378 | if out is None:
379 | out = sys.stdout
380 |
381 | try:
382 | line = line.decode()
383 | except (UnicodeDecodeError, AttributeError):
384 | pass
385 |
386 | message = line + endLine
387 | message = tools.stdoutEncode(message)
388 | try:
389 | out.write(message)
390 | except:
391 | pass
392 | out.flush()
393 |
394 |
395 | def printAbout():
396 | printLine("Version: %s" % __version__)
397 | printLine("Geeknote - a command line client for Evernote.")
398 | printLine("Use geeknote --help to read documentation.")
399 |
--------------------------------------------------------------------------------
/geeknote/storage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import datetime
5 | import pickle
6 |
7 | from sqlalchemy import *
8 | from sqlalchemy.orm import *
9 | from sqlalchemy.ext.declarative import declarative_base
10 | from sqlalchemy.orm import sessionmaker
11 |
12 | import logging
13 | from . import config
14 |
15 | db_path = os.path.join(config.APP_DIR, "database.db")
16 | engine = create_engine("sqlite:///" + db_path)
17 | Base = declarative_base()
18 |
19 |
20 | class Userprop(Base):
21 | __tablename__ = "user_props"
22 |
23 | id = Column(Integer, primary_key=True)
24 | key = Column(String(255))
25 | value = Column(PickleType())
26 |
27 | def __init__(self, key, value):
28 | self.key = key
29 | self.value = value
30 |
31 | def __repr__(self):
32 | return "".format(self.key, self.value)
33 |
34 |
35 | class Setting(Base):
36 | __tablename__ = "settings"
37 |
38 | id = Column(Integer, primary_key=True)
39 | key = Column(String(255))
40 | value = Column(String(1000))
41 |
42 | def __init__(self, key, value):
43 | self.key = key
44 | self.value = value
45 |
46 | def __repr__(self):
47 | return "".format(self.key, self.value)
48 |
49 |
50 | class Notebook(Base):
51 | __tablename__ = "notebooks"
52 |
53 | id = Column(Integer, primary_key=True)
54 | name = Column(String(255))
55 | guid = Column(String(1000))
56 | timestamp = Column(DateTime(), nullable=False)
57 |
58 | def __init__(self, guid, name):
59 | self.guid = guid
60 | self.name = name
61 | self.timestamp = datetime.datetime.now()
62 |
63 | def __repr__(self):
64 | return "".format(self.name)
65 |
66 |
67 | class Tag(Base):
68 | __tablename__ = "tags"
69 |
70 | id = Column(Integer, primary_key=True)
71 | tag = Column(String(255))
72 | guid = Column(String(1000))
73 | timestamp = Column(DateTime(), nullable=False)
74 |
75 | def __init__(self, guid, tag):
76 | self.guid = guid
77 | self.tag = tag
78 | self.timestamp = datetime.datetime.now()
79 |
80 | def __repr__(self):
81 | return "".format(self.tag)
82 |
83 |
84 | class Note(Base):
85 | __tablename__ = "notes"
86 |
87 | id = Column(Integer, primary_key=True)
88 | guid = Column(String(1000))
89 | obj = Column(PickleType())
90 | timestamp = Column(DateTime(), nullable=False)
91 |
92 | def __init__(self, guid, obj):
93 | self.guid = guid
94 | self.obj = obj
95 | self.timestamp = datetime.datetime.now()
96 |
97 | def __repr__(self):
98 | return "".format(self.timestamp)
99 |
100 |
101 | class Search(Base):
102 | __tablename__ = "search"
103 |
104 | id = Column(Integer, primary_key=True)
105 | search_obj = Column(PickleType())
106 | timestamp = Column(DateTime(), nullable=False)
107 |
108 | def __init__(self, search_obj):
109 | self.search_obj = search_obj
110 | self.timestamp = datetime.datetime.now()
111 |
112 | def __repr__(self):
113 | return "".format(self.timestamp)
114 |
115 |
116 | class Storage(object):
117 | """
118 | Class for using database
119 | """
120 |
121 | session = None
122 |
123 | def __init__(self):
124 | logging.debug("Storage engine : %s", engine)
125 | Base.metadata.create_all(engine)
126 | Session = sessionmaker(bind=engine)
127 | self.session = Session()
128 |
129 | def logging(func):
130 | def wrapper(*args, **kwargs):
131 | try:
132 | return func(*args, **kwargs)
133 | except Exception as e:
134 | logging.error("%s : %s", func.__name__, str(e))
135 | return False
136 |
137 | return wrapper
138 |
139 | @logging
140 | def createUser(self, oAuthToken, info_obj):
141 | """
142 | Create a user
143 | oAuthToken must be not empty string
144 | info_obj must be not empty string
145 | Previous user and user's properties will be removed
146 | returns True if all done
147 | """
148 | if not oAuthToken:
149 | raise Exception("Empty oAuth token")
150 |
151 | if not info_obj:
152 | raise Exception("Empty user info")
153 |
154 | for item in self.session.query(Userprop).all():
155 | self.session.delete(item)
156 |
157 | self.setUserprop("oAuthToken", oAuthToken)
158 | self.setUserprop("info", info_obj)
159 |
160 | return True
161 |
162 | @logging
163 | def removeUser(self):
164 | """
165 | Remove user
166 | returns True if all done
167 | """
168 | for item in self.session.query(Userprop).all():
169 | self.session.delete(item)
170 | self.session.commit()
171 | return True
172 |
173 | @logging
174 | def getUserToken(self):
175 | """
176 | Get user's oAuth token
177 | returns oAuth token if it exists
178 | returns None if there is no oAuth token yet
179 | """
180 | return self.getUserprop("oAuthToken")
181 |
182 | @logging
183 | def getUserInfo(self):
184 | """
185 | Get user's "info" property value
186 | returns info property value if it exists
187 | returns None if there is no info property
188 | """
189 | return self.getUserprop("info")
190 |
191 | @logging
192 | def getUserprops(self):
193 | """
194 | Get all user properties
195 | returns list of dict if all done
196 | returns [] if there are not any user properties yet
197 | """
198 | props = self.session.query(Userprop).all()
199 | return [{item.key: pickle.loads(item.value)} for item in props]
200 |
201 | @logging
202 | def getUserprop(self, key):
203 | """
204 | Get user property by key
205 | returns property's value if it the property exists
206 | returns None if the property doesn't exist
207 | """
208 | instance = self.session.query(Userprop).filter_by(key=key).first()
209 | if instance:
210 | return pickle.loads(instance.value)
211 | else:
212 | return None
213 |
214 | @logging
215 | def setUserprop(self, key, value):
216 | """
217 | Set a single user property
218 | User's property must have key and value
219 | returns True if all done
220 | """
221 | value = pickle.dumps(value)
222 |
223 | instance = self.session.query(Userprop).filter_by(key=key).first()
224 | if instance:
225 | instance.value = value
226 | else:
227 | instance = Userprop(key, value)
228 | self.session.add(instance)
229 |
230 | self.session.commit()
231 | return True
232 |
233 | @logging
234 | def delUserprop(self, key):
235 | """
236 | Delete a single user property
237 | User property must have key
238 | returns True if all done
239 | returns False if property didn't exist
240 | """
241 | instance = self.session.query(Userprop).filter_by(key=key).first()
242 | if instance:
243 | self.session.delete(instance)
244 | return True
245 | return False
246 |
247 | @logging
248 | def setSettings(self, settings):
249 | """
250 | Set multiple settings
251 | Settings must be an instance dict
252 | returns True if all done
253 | """
254 | if not isinstance(settings, dict):
255 | raise Exception("Wrong settings")
256 |
257 | for key in list(settings.keys()):
258 | if not settings[key]:
259 | raise Exception("Wrong setting's item")
260 |
261 | instance = self.session.query(Setting).filter_by(key=key).first()
262 | if instance:
263 | instance.value = pickle.dumps(settings[key])
264 | else:
265 | instance = Setting(key, pickle.dumps(settings[key]))
266 | self.session.add(instance)
267 |
268 | self.session.commit()
269 | return True
270 |
271 | @logging
272 | def getSettings(self):
273 | """
274 | Get all settings
275 | returns list of dict if all done
276 | returns {} there are not any settings yet
277 | """
278 | settings = self.session.query(Setting).all()
279 | result = {}
280 | for item in settings:
281 | result[item.key] = pickle.loads(item.value)
282 | return result
283 |
284 | @logging
285 | def setSetting(self, key, value):
286 | """
287 | Set single setting
288 | Setting must have key and value
289 | returns True if all done
290 | """
291 | instance = self.session.query(Setting).filter_by(key=key).first()
292 | if instance:
293 | instance.value = value
294 | else:
295 | instance = Setting(key, value)
296 | self.session.add(instance)
297 |
298 | self.session.commit()
299 | return True
300 |
301 | @logging
302 | def getSetting(self, key):
303 | """
304 | Get a setting by key
305 | returns setting's value if setting exists
306 | returns None if setting doesn't exist
307 | """
308 | instance = self.session.query(Setting).filter_by(key=key).first()
309 | if instance:
310 | return instance.value
311 | else:
312 | return None
313 |
314 | @logging
315 | def setTags(self, tags):
316 | """
317 | Set tags
318 | Tags must be an instance of dict
319 | Previous tags items will be removed
320 | returns True if all done
321 | """
322 | if not isinstance(tags, dict):
323 | raise Exception("Wrong tags")
324 |
325 | for item in self.session.query(Tag).all():
326 | self.session.delete(item)
327 |
328 | for key in list(tags.keys()):
329 | if not tags[key]:
330 | raise Exception("Wrong tag's item")
331 |
332 | instance = Tag(key, tags[key])
333 | self.session.add(instance)
334 |
335 | self.session.commit()
336 | return True
337 |
338 | @logging
339 | def getTags(self):
340 | """
341 | Get all tags
342 | returns list of dicts of tags if all done
343 | returns {} if there are not any tags yet
344 | """
345 | tags = self.session.query(Tag).all()
346 | result = {}
347 | for item in tags:
348 | result[item.guid] = item.tag
349 | return result
350 |
351 | @logging
352 | def setNotebooks(self, notebooks):
353 | """
354 | Set notebooks
355 | Notebooks must be an instance of dict
356 | Previous notebooks items will be removed
357 | returns True if all done
358 | """
359 | if not isinstance(notebooks, dict):
360 | raise Exception("Wrong notebooks")
361 |
362 | for item in self.session.query(Notebook).all():
363 | self.session.delete(item)
364 |
365 | for key in list(notebooks.keys()):
366 | if not notebooks[key]:
367 | raise Exception("Wrong notebook's item")
368 |
369 | instance = Notebook(key, notebooks[key])
370 | self.session.add(instance)
371 |
372 | self.session.commit()
373 | return True
374 |
375 | @logging
376 | def getNotebooks(self):
377 | """
378 | Get all notebooks
379 | returns list of notebooks if all done
380 | returns {} there are not any notebooks yet
381 | """
382 | notebooks = self.session.query(Notebook).all()
383 | result = {}
384 | for item in notebooks:
385 | result[item.guid] = item.name
386 | return result
387 |
388 | @logging
389 | def setNote(self, obj):
390 | """
391 | Remembers a note, indexed by GUID
392 | Note can be retrieved by GUID later
393 | returns True
394 | """
395 | for item in self.session.query(Note).filter(Note.guid == obj.guid).all():
396 | self.session.delete(item)
397 |
398 | note = pickle.dumps(obj)
399 | instance = Note(obj.guid, note)
400 | self.session.add(instance)
401 |
402 | self.session.commit()
403 | return True
404 |
405 | @logging
406 | def getNote(self, guid):
407 | """
408 | Get note by GUID
409 | returns note if found by GUID
410 | returns None if note is not found
411 | """
412 | note = self.session.query(Note).filter(Note.guid == guid).first()
413 | if note:
414 | return pickle.loads(note.obj)
415 | else:
416 | return None
417 |
418 | @logging
419 | def setSearch(self, search_obj):
420 | """
421 | Remembers a search
422 | Previous searching items will be removed
423 | return True if all done
424 | """
425 | for item in self.session.query(Search).all():
426 | self.session.delete(item)
427 |
428 | search = pickle.dumps(search_obj)
429 | instance = Search(search)
430 | self.session.add(instance)
431 |
432 | self.session.commit()
433 | return True
434 |
435 | @logging
436 | def getSearch(self):
437 | """
438 | Get last search
439 | return last search if it exists
440 | return None there are not any searches yet
441 | """
442 | search = self.session.query(Search).first()
443 | if search:
444 | return pickle.loads(search.search_obj)
445 | else:
446 | return None
447 |
--------------------------------------------------------------------------------
/geeknote/tools.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import out
4 | import sys
5 | import time
6 |
7 |
8 | def checkIsInt(value):
9 | try:
10 | int(value)
11 | return True
12 | except ValueError:
13 | return False
14 |
15 |
16 | def getch():
17 | """
18 | Pause program until any keyboard key is pressed
19 | (Gets a character from the console without echo)
20 | """
21 | try:
22 | import msvcrt
23 |
24 | return msvcrt.getch()
25 |
26 | except ImportError:
27 | import sys
28 | import tty
29 | import termios
30 |
31 | fd = sys.stdin.fileno()
32 | old_settings = termios.tcgetattr(fd)
33 | try:
34 | tty.setraw(sys.stdin.fileno())
35 | ch = sys.stdin.read(1)
36 | finally:
37 | termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
38 | return ch
39 |
40 |
41 | def strip(data):
42 | if not data:
43 | return data
44 |
45 | if isinstance(data, dict):
46 | items = iter(data.items())
47 | return dict([[key.strip(" \t\n\r\"'"), val] for key, val in items])
48 |
49 | if isinstance(data, list):
50 | return [val.strip(" \t\n\r\"'") for val in data]
51 |
52 | if isinstance(data, str):
53 | return data.strip(" \t\n\r\"'")
54 |
55 | raise Exception("Unexpected args type: " "%s. Expect list or dict" % type(data))
56 |
57 |
58 | class ExitException(Exception):
59 | pass
60 |
61 |
62 | def _exit(message, code):
63 | out.preloader.exit(code)
64 | time.sleep(0.33)
65 | raise ExitException(message)
66 |
67 |
68 | def exit(message="exit", code=0):
69 | _exit(message, code)
70 |
71 |
72 | def exitErr(message="exit", code=1):
73 | _exit(message, code)
74 |
75 |
76 | class Struct:
77 | def __init__(self, **entries):
78 | self.__dict__.update(entries)
79 |
80 |
81 | def decodeArgs(args):
82 | return [stdinEncode(val) for val in args]
83 |
84 |
85 | def stdoutEncode(data):
86 | # this is not for logging output, it is for output from geeknote queries to evernote
87 | if isinstance(sys.stdout.encoding, str) and sys.stdout.encoding != 'utf-8':
88 | return data.encode('utf-8').decode(sys.stdout.encoding)
89 | else:
90 | return data
91 |
92 |
93 | def stdinEncode(data):
94 | if isinstance(sys.stdin.encoding, str) and sys.stdin.encoding.lower() != 'utf-8':
95 | return data.encode(sys.stdin.encoding).decode('utf-8')
96 | else:
97 | return data
98 |
--------------------------------------------------------------------------------
/proxy_support.md:
--------------------------------------------------------------------------------
1 | HTTP proxy support for geeknote
2 | ===============================
3 |
4 | I recommend to make this work with virtualenv, to avoid overwriting system files.
5 | The important part is to install in the order **thrift, then evernote, then geeknote**. This will make sure that path search order is correct for thrift.
6 |
7 | ```
8 | # Download thrift and geeknote
9 | git clone https://github.com/apache/thrift.git
10 | git clone https://github.com/mwilck/geeknote.git
11 |
12 | # create and enter a virtual environment
13 | virtualenv /var/tmp/geeknote
14 | . /var/tmp/geeknote/bin/activate
15 |
16 | # Apply proxy-support patches for thrift
17 | cd thrift
18 |
19 | ## If the patches don't apply, you may need to check out the state that I wrote the patches for:
20 | ## git checkout -b proxy e363a34e63
21 | curl https://issues.apache.org/jira/secure/attachment/12801233/0001-python-THttpClient-Add-support-for-system-proxy-sett.patch | git am
22 | curl https://issues.apache.org/jira/secure/attachment/12801234/0002-Python-THttpClient-Support-proxy-authorization.patch | git am
23 |
24 | # Install thrift from the patched tree
25 | (cd lib/py; python setup.py install)
26 | cd ..
27 |
28 | # Install evernote
29 | pip install evernote
30 |
31 | # Install geeknote
32 | cd geeknote
33 | python setup.py install
34 | ```
35 |
36 | Now `geeknote login`, `geeknote find`, etc. should work behind a proxy if the `http_proxy` environment variable is correctly set. You can now generate a script to activate the virtual environment:
37 |
38 | ```
39 | cat >~/bin/geeknote <<\EOF
40 | #! /bin/bash
41 | . /var/tmp/geeknote/bin/activate
42 | exec geeknote "$@"
43 | EOF
44 | chmod a+x ~/bin/geeknote
45 | ```
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | evernote3
2 | html2text
3 | sqlalchemy
4 | markdown2
5 | thrift
6 | beautifulsoup4
7 | proxyenv
8 | lxml
9 |
10 | # Testing requirements
11 | pytest
12 | pytest-runner
13 | mock
14 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | test=pytest
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import sys
5 | import os
6 | import shutil
7 | import codecs
8 | from setuptools import setup
9 | from setuptools.command.install import install
10 | import traceback
11 |
12 |
13 | def read(fname):
14 | return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read()
15 |
16 |
17 | class full_install(install):
18 |
19 | user_options = install.user_options + [
20 | (
21 | "bash-completion-dir=",
22 | None,
23 | "(Linux only) Set bash completion directory (default: /etc/bash_completion.d)",
24 | ),
25 | (
26 | "zsh-completion-dir=",
27 | None,
28 | "(Linux only) Set zsh completion directory (default: /usr/local/share/zsh/site-functions)",
29 | ),
30 | ]
31 |
32 | def initialize_options(self):
33 | install.initialize_options(self)
34 | self.bash_completion_dir = "/etc/bash_completion.d"
35 | self.zsh_completion_dir = "/usr/local/share/zsh/site-functions"
36 |
37 | def run(self):
38 | if sys.platform.startswith("linux"):
39 | self.install_autocomplete()
40 | install.run(self)
41 |
42 | def install_autocomplete(self):
43 | def copy_autocomplete(src, dst):
44 | try:
45 | if os.path.exists(dst):
46 | shutil.copy(src, dst)
47 | print("copying %s -> %s" % (src, dst))
48 | except IOError:
49 | print(
50 | "cannot copy autocomplete script %s to %s, got root ?" % (src, dst)
51 | )
52 | print(traceback.format_exc())
53 |
54 | print("installing autocomplete")
55 | copy_autocomplete(
56 | "completion/bash_completion/_geeknote", self.bash_completion_dir
57 | )
58 | copy_autocomplete(
59 | "completion/zsh_completion/_geeknote", self.zsh_completion_dir
60 | )
61 |
62 |
63 | with open("geeknote/__init__.py") as f:
64 | exec (f.read()) # read __version__
65 |
66 | setup(
67 | name="geeknote",
68 | version=__version__,
69 | license="GPL",
70 | author="Jeff Kowalski",
71 | # author_email='',
72 | description="Geeknote - is a command line client for Evernote, "
73 | "that can be used on Linux, FreeBSD and OS X.",
74 | long_description=read("README.md"),
75 | url="http://github.com/jeffkowalski/geeknote",
76 | packages=["geeknote"],
77 | classifiers=[
78 | "Development Status :: 5 - Production/Stable",
79 | "Intended Audience :: End Users/Desktop",
80 | "License :: OSI Approved :: GNU General Public License (GPL)",
81 | "Environment :: Console",
82 | "Natural Language :: English",
83 | "Operating System :: OS Independent",
84 | "Programming Language :: Python",
85 | "Topic :: Utilities",
86 | ],
87 | install_requires=[
88 | "evernote3",
89 | "html2text",
90 | "sqlalchemy",
91 | "markdown2",
92 | "beautifulsoup4",
93 | "thrift",
94 | "proxyenv",
95 | "lxml",
96 | ],
97 | setup_requires=["pytest-runner"],
98 | tests_require=["mock", "pytest"],
99 | entry_points={
100 | "console_scripts": [
101 | "geeknote = geeknote.geeknote:main",
102 | "gnsync = geeknote.gnsync:main",
103 | ]
104 | },
105 | cmdclass={"install": full_install},
106 | platforms="Any",
107 | zip_safe=True,
108 | keywords="Evernote, console",
109 | )
110 |
--------------------------------------------------------------------------------
/tests/fixtures/Test Note with non-ascii.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | This is a test note written in the Evernote online editor. it includes some non-ascii characters:
—
–
−
€ Œ “” ™ œ ž © µ ¶ å õ ý þ ß Ü
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | from mock import Mock
3 |
4 |
5 | # See https://stackoverflow.com/a/16976500/1299271
6 | class AnyStringWith(str):
7 | """
8 | Matches with Mock function string arguments that contain a provided
9 | substring.
10 | """
11 | def __eq__(self, other):
12 | return self in other
13 |
14 |
15 | # See https://stackoverflow.com/a/54838760/1299271
16 | def assert_not_called_with(self, *args, **kwargs):
17 | """
18 | Asserts that a Mock function was never called with specified arguments.
19 | """
20 | try:
21 | self.assert_called_with(*args, **kwargs)
22 | except AssertionError:
23 | return
24 | raise AssertionError('Expected %s to not have been called.' % self._format_mock_call_signature(args, kwargs))
25 |
26 |
27 | Mock.assert_not_called_with = assert_not_called_with
28 |
--------------------------------------------------------------------------------
/tests/pseudoedit.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import re
3 | import unittest
4 |
5 |
6 | class pseudoTest(unittest.TestCase):
7 | def test_dummy(self):
8 | self.assertTrue(1)
9 |
10 |
11 | def do_edit(filename, filter=None):
12 | with open(filename, "r") as f:
13 | lines = f.readlines()
14 | with open(filename, "w") as f:
15 | for line in lines:
16 | if filter is not None and not filter.match(line):
17 | f.write(line)
18 |
19 | if __name__ == "__main__":
20 | # delete all lines containing the word "delete"
21 | filter = re.compile(r".*\bdelete\b")
22 | for name in sys.argv[1:]:
23 | do_edit(name, filter=filter)
24 |
--------------------------------------------------------------------------------
/tests/test_argparser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from geeknote.argparser import *
4 | from io import StringIO
5 | import sys
6 | import unittest
7 |
8 |
9 | class testArgparser(unittest.TestCase):
10 |
11 | def setUp(self):
12 | sys.stdout = StringIO() # set fake stdout
13 |
14 | # добавляем тестовые данные
15 | COMMANDS_DICT['testing'] = {
16 | "help": "Create note",
17 | "firstArg": "--test_req_arg",
18 | "arguments": {
19 | "--test_req_arg": {"altName": "-tra",
20 | "help": "Set note title",
21 | "required": True},
22 | "--test_arg": {"altName": "-ta",
23 | "help": "Add tag to note",
24 | "emptyValue": None},
25 | "--test_arg2": {"altName": "-ta2", "help": "Add tag to note"},
26 | },
27 | "flags": {
28 | "--test_flag": {"altName": "-tf",
29 | "help": "Add tag to note",
30 | "value": True,
31 | "default": False},
32 | }
33 | }
34 |
35 | def testEmptyCommand(self):
36 | parser = argparser([])
37 | self.assertFalse(parser.parse(), False)
38 |
39 | def testErrorCommands(self):
40 | parser = argparser(["testing_err", ])
41 | self.assertFalse(parser.parse(), False)
42 |
43 | def testErrorArg(self):
44 | parser = argparser(["testing", "test_def_val", "--test_arg_err"])
45 | self.assertEqual(parser.parse(), False)
46 |
47 | def testErrorNoArg(self):
48 | parser = argparser(["testing"])
49 | self.assertEqual(parser.parse(), False)
50 |
51 | def testErrorReq(self):
52 | parser = argparser(["testing", "--test_arg", "test_val"])
53 | self.assertEqual(parser.parse(), False)
54 |
55 | def testErrorVal(self):
56 | parser = argparser(["testing", "--test_req_arg", '--test_arg'])
57 | self.assertEqual(parser.parse(), False)
58 |
59 | def testErrorFlag(self):
60 | parser = argparser(["testing", "--test_flag", 'test_val'])
61 | self.assertEqual(parser.parse(), False)
62 |
63 | def testSuccessCommand1(self):
64 | parser = argparser(["testing", "--test_req_arg", "test_req_val",
65 | "--test_flag", "--test_arg", "test_val"])
66 | self.assertEqual(parser.parse(), {"test_req_arg": "test_req_val",
67 | "test_flag": True,
68 | "test_arg": "test_val"})
69 |
70 | def testSuccessCommand2(self):
71 | parser = argparser(["testing", "test_req_val", "--test_flag",
72 | "--test_arg", "test_val"])
73 | self.assertEqual(parser.parse(), {"test_req_arg": "test_req_val",
74 | "test_flag": True,
75 | "test_arg": "test_val"})
76 |
77 | def testSuccessCommand3(self):
78 | parser = argparser(["testing", "test_def_val"])
79 | self.assertEqual(parser.parse(), {"test_req_arg": "test_def_val",
80 | "test_flag": False})
81 |
82 | def testSuccessCommand4(self):
83 | parser = argparser(["testing", "test_def_val", "--test_arg"])
84 | self.assertEqual(parser.parse(), {"test_req_arg": "test_def_val",
85 | "test_arg": None,
86 | "test_flag": False})
87 |
88 | def testSuccessCommand5(self):
89 | parser = argparser(["testing", "test_def_val", "--test_arg",
90 | "--test_arg2", "test_arg2_val"])
91 | self.assertEqual(parser.parse(), {"test_req_arg": "test_def_val",
92 | "test_arg": None,
93 | "test_arg2": "test_arg2_val",
94 | "test_flag": False})
95 |
96 | def testSuccessShortAttr(self):
97 | parser = argparser(["testing", "test_def_val", "-ta",
98 | "-ta2", "test_arg2_val"])
99 | self.assertEqual(parser.parse(), {"test_req_arg": "test_def_val",
100 | "test_arg": None,
101 | "test_arg2": "test_arg2_val",
102 | "test_flag": False})
103 |
104 | def testSuccessShortAttr2(self):
105 | parser = argparser(["testing", "-tra", "test_def_val", "-tf"])
106 | self.assertEqual(parser.parse(), {"test_req_arg": "test_def_val",
107 | "test_flag": True})
108 |
--------------------------------------------------------------------------------
/tests/test_editor.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from geeknote.editor import Editor
4 | import unittest
5 |
6 |
7 | class testEditor(unittest.TestCase):
8 |
9 | def setUp(self):
10 | self.MD_TEXT = """# Header 1
11 |
12 | ## Header 2
13 |
14 | Line 1
15 |
16 | _Line 2_
17 |
18 | **Line 3**
19 |
20 | """
21 | self.HTML_TEXT = """Header 1
22 | Header 2
23 | Line 1
24 | Line 2
25 | Line 3
26 | """
27 |
28 | def test_TextToENML(self):
29 | self.assertEqual(Editor.textToENML(self.MD_TEXT),
30 | Editor.wrapENML(self.HTML_TEXT))
31 |
32 | def test_ENMLToText(self):
33 | wrapped = Editor.wrapENML(self.HTML_TEXT)
34 | self.assertEqual(Editor.ENMLtoText(wrapped), self.MD_TEXT)
35 |
36 | def test_TODO(self):
37 | MD_TODO = "\n* [ ]item 1\n\n* [x]item 2\n\n* [ ]item 3\n\n"
38 | HTML_TODO = "item 1
item 2
item 3
\n"
39 | self.assertEqual(Editor.textToENML(MD_TODO),
40 | Editor.wrapENML(HTML_TODO))
41 |
42 | wrapped = Editor.wrapENML(HTML_TODO)
43 | text = Editor.ENMLtoText(wrapped)
44 | self.assertEqual(text, MD_TODO)
45 |
46 | def test_htmlEscape(self):
47 | wrapped = Editor.textToENML(content="", format="markdown")
48 | self.assertEqual(wrapped, """
49 |
50 | <what ever>
51 | """)
52 |
53 | def test_not_checklist(self):
54 | wrapped = Editor.textToENML(content=" Item head[0]; ", format="markdown")
55 | self.assertEqual(wrapped, """
56 |
57 | Item head[0];
58 | """)
59 |
60 | def test_wrapENML_success(self):
61 | text = "test"
62 | result = '''
63 |
64 | test'''
65 | self.assertEqual(Editor.wrapENML(text), result)
66 |
67 | def test_wrapENML_without_argument_fail(self):
68 | self.assertRaises(TypeError, Editor.wrapENML)
69 |
--------------------------------------------------------------------------------
/tests/test_gclient.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import unittest
4 |
5 | from geeknote.gclient import CustomClient
6 | from geeknote.geeknote import UserStore
7 |
8 |
9 | class testGclient(unittest.TestCase):
10 |
11 | def test_patched_client(self):
12 | self.assertEqual(UserStore.Client, CustomClient)
13 |
14 | def test_patched_client_contain_methods(self):
15 | METHODS = dir(UserStore.Client)
16 | self.assertIn('getNoteStoreUrl', METHODS)
17 | self.assertIn('send_getNoteStoreUrl', METHODS)
18 | self.assertIn('recv_getNoteStoreUrl', METHODS)
19 |
--------------------------------------------------------------------------------
/tests/test_geeknote.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | import time
5 | import unittest
6 | from io import StringIO
7 | from geeknote.geeknote import *
8 | from geeknote import tools
9 | from geeknote.editor import Editor
10 | from geeknote.storage import Storage
11 |
12 |
13 | class GeekNoteOver(GeekNote):
14 | def __init__(self):
15 | pass
16 |
17 | def loadNoteContent(self, note):
18 | if "content" not in note.__dict__:
19 | note.content = "note content"
20 |
21 | def updateNote(self, guid=None, **inputData):
22 | # HACK for testing: this assumes that the guid represents a "note" itself
23 | # see check_editWithEditorInThread below
24 | guid.content = inputData["content"]
25 |
26 |
27 | class NotesOver(Notes):
28 | def connectToEvernote(self):
29 | self.evernote = GeekNoteOver()
30 |
31 |
32 | class testNotes(unittest.TestCase):
33 |
34 | @classmethod
35 | def setUpClass(cls):
36 | # Use our trivial "pseudoedit" program as editor to avoid user interaction
37 | cls.storage = Storage()
38 | cls.saved_editor = cls.storage.getUserprop('editor')
39 | cls.storage.setUserprop('editor', sys.executable + " " +
40 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "pseudoedit.py"))
41 |
42 | @classmethod
43 | def tearDownClass(cls):
44 | if cls.saved_editor:
45 | cls.storage.setUserprop('editor', cls.saved_editor)
46 |
47 | def setUp(self):
48 | self.notes = NotesOver()
49 | self.testNote = tools.Struct(title="note title")
50 | # set the timezone for the date tests to work
51 | # this is particularly important on Travis CI, where
52 | # the timezone may not be the same as our dev machine
53 | os.environ['TZ'] = "PST-0800"
54 | time.tzset()
55 |
56 | def test_parseInput1(self):
57 | testData = self.notes._parseInput("title", "test body", ["tag1"], None, None, ["res 1", "res 2"])
58 | self.assertTrue(isinstance(testData, dict))
59 | if not isinstance(testData, dict):
60 | return
61 |
62 | self.assertEqual(testData['title'], "title")
63 | self.assertEqual(testData['content'], Editor.textToENML("test body"))
64 | self.assertEqual(testData["tags"], ["tag1"])
65 | self.assertEqual(testData["resources"], ["res 1", "res 2"])
66 |
67 | def test_parseInput2(self):
68 | testData = self.notes._parseInput("title", "WRITE", ["tag1", "tag2"], None, None, self.testNote)
69 | self.assertTrue(isinstance(testData, dict))
70 | if not isinstance(testData, dict):
71 | return
72 |
73 | self.assertEqual(testData['title'], "title")
74 | self.assertEqual(
75 | testData['content'],
76 | "WRITE"
77 | )
78 | self.assertEqual(testData["tags"], ["tag1", "tag2"])
79 |
80 | # this doesn't seem to work in Python 3 - need help
81 | def check_editWithEditorInThread(self, txt, expected):
82 | testNote = tools.Struct(title="note title",
83 | content=txt)
84 | # hack to make updateNote work - see above
85 | testNote.guid = testNote
86 | testData = self.notes._parseInput("title",
87 | txt,
88 | ["tag1", "tag2"],
89 | None, None, testNote)
90 | result = self.notes._editWithEditorInThread(testData, testNote)
91 | self.assertEqual(Editor.ENMLtoText(testNote.content), expected)
92 |
93 | # def test_editWithEditorInThread(self):
94 | # txt = "Please do not change this file"
95 | # self.check_editWithEditorInThread(txt, txt + '\n')
96 |
97 | def test_editWithEditorInThread2(self):
98 | txt = "Please delete this line, save, and quit the editor"
99 | self.check_editWithEditorInThread(txt, "\n\n")
100 |
101 | def test_createSearchRequest1(self):
102 | testRequest = self.notes._createSearchRequest(
103 | search="test text",
104 | tags=["tag1"],
105 | notebook="test notebook",
106 | date="2000-01-01",
107 | exact_entry=True,
108 | content_search=True
109 | )
110 | response = 'notebook:"test notebook" tag:tag1' \
111 | ' created:19991231T000000Z "test text"'
112 | self.assertEqual(testRequest, response)
113 |
114 | def test_createSearchRequest2(self):
115 | testRequest = self.notes._createSearchRequest(
116 | search="test text",
117 | tags=["tag1", "tag2"],
118 | notebook="notebook1",
119 | date="1999-12-31/2000-12-31",
120 | exact_entry=False,
121 | content_search=False
122 | )
123 | response = 'notebook:notebook1 tag:tag1' \
124 | ' tag:tag2 created:19991230T000000Z -created:20001231T000000Z' \
125 | ' intitle:test text'
126 | self.assertEqual(testRequest, response)
127 |
128 | def testError_createSearchRequest1(self):
129 | # set fake stdout and stderr
130 | self.stdout, sys.stdout = sys.stdout, StringIO()
131 | self.stderr, sys.stderr = sys.stderr, StringIO()
132 | sys.exit = lambda code: code
133 | with self.assertRaises(tools.ExitException):
134 | self.notes._createSearchRequest(search="test text",
135 | date="12-31-1999")
136 |
--------------------------------------------------------------------------------
/tests/test_gnsync.py:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | from mock import patch, ANY, Mock
3 | import os
4 | import unittest
5 | import shutil
6 | from helpers import AnyStringWith
7 | from geeknote.gnsync import remove_control_characters, GNSync
8 |
9 |
10 | class testGnsync(unittest.TestCase):
11 | def setUp(self):
12 | self.test_dir = '/tmp/test_gnsync'
13 | if not os.path.exists(self.test_dir):
14 | os.makedirs(self.test_dir)
15 |
16 | self.given_eng = '\0This is an english\1 sentence. Is it ok?'
17 | self.expected_eng = 'This is an english sentence. Is it ok?'
18 | self.given_kor = '한국\2어입니\3다. 잘 되나요?'
19 | self.expected_kor = '한국어입니다. 잘 되나요?'
20 | self.given_chn = '中\4国的输入。我令\5您着迷?'
21 | self.expected_chn = '中国的输入。我令您着迷?'
22 | self.given_combined = self.expected_combined = """# 제목
23 |
24 | ## 제 1 장
25 |
26 | _한국어 입력입니다. 잘 되나요?_
27 |
28 | ## 第 2 章
29 |
30 | *中国的输入。我令您着迷?*
31 |
32 | ## Chapter 3
33 |
34 | - May the force be with you!
35 |
36 | """
37 |
38 | def tearDown(self):
39 | if os.path.exists(self.test_dir):
40 | shutil.rmtree(self.test_dir)
41 |
42 | def test_strip_eng(self):
43 | self.assertEqual(remove_control_characters(self.given_eng),
44 | self.expected_eng)
45 |
46 | def test_strip_kor(self):
47 | self.assertEqual(remove_control_characters(self.given_kor),
48 | self.expected_kor)
49 |
50 | def test_strip_chn(self):
51 | self.assertEqual(remove_control_characters(self.given_chn),
52 | self.expected_chn)
53 |
54 | def test_strip_nochange(self):
55 | self.assertEqual(remove_control_characters(self.given_combined),
56 | self.expected_combined)
57 |
58 | @patch('geeknote.gnsync.logger', autospec=True)
59 | @patch('geeknote.gnsync.GeekNote', autospec=True)
60 | @patch('geeknote.gnsync.Storage', autospec=True)
61 | def test_create_file_with_non_ascii_chars(self, mock_storage, mock_geeknote, mock_logger):
62 |
63 | # Mock GeekNote#loadNoteContent to provide some content with non-ascii
64 | def mock_note_load(note):
65 | with open('tests/fixtures/Test Note with non-ascii.xml', 'r') as f:
66 | note.content = f.read()
67 | note.notebookName = 'testNotebook'
68 | mock_geeknote.return_value.loadNoteContent.side_effect = mock_note_load
69 |
70 | # Mock Storage().getUserToken() so the GNSync constructor doesn't throw
71 | # an exception
72 | mock_storage.return_value.getUserToken.return_value = True
73 |
74 | subject = GNSync('test_notebook', self.test_dir, '*.*',
75 | 'plain', download_only=True)
76 |
77 | mock_note = Mock()
78 | mock_note.guid = '123abc'
79 | mock_note.title = 'Test Note'
80 |
81 | subject._create_file(mock_note)
82 |
83 | # Verify that a file could be created with non-ascii content
84 | mock_logger.exception.assert_not_called_with(
85 | ANY,
86 | AnyStringWith("codec can't decode byte")
87 | )
88 |
89 | with open(self.test_dir + "/Test Note.txt", 'r', encoding='utf-8') as f:
90 | self.assertIn("œ ž © µ ¶ å õ ý þ ß Ü", f.read())
91 |
--------------------------------------------------------------------------------
/tests/test_out.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import sys
4 | import os
5 | import time
6 | import unittest
7 | from io import StringIO
8 | from geeknote import __version__
9 | from geeknote.config import USER_BASE_URL
10 | from geeknote.out import printDate, printLine, printAbout,\
11 | separator, failureMessage, successMessage, showUser, showNote, \
12 | printList, SearchResult
13 | from geeknote import out
14 |
15 |
16 | class AccountingStub(object):
17 | uploadLimit = 100
18 | uploadLimitEnd = 1095292800000
19 |
20 |
21 | class UserStub(object):
22 | username = 'testusername'
23 | name = 'testname'
24 | email = 'testemail'
25 | id = 111
26 | shardId = 222
27 | accounting = AccountingStub()
28 | timezone = None
29 |
30 |
31 | class AttributesStub(object):
32 | reminderOrder = None
33 | reminderTime = None
34 | reminderDoneTime = None
35 |
36 |
37 | class NoteStub(object):
38 | title = 'testnote'
39 | created = 1095292800000
40 | updated = 1095292800000
41 | content = '##note content'
42 | tagNames = ['tag1', 'tag2', 'tag3']
43 | notebookName = 'geeknotes'
44 | guid = 12345
45 | attributes = AttributesStub()
46 |
47 |
48 | class outTestsWithHackedStdout(unittest.TestCase):
49 |
50 | def setUp(self):
51 | # set fake stdout and stderr
52 | self.stdout, sys.stdout = sys.stdout, StringIO()
53 | self.stderr, sys.stderr = sys.stderr, StringIO()
54 | # set the timezone for the date tests to work
55 | # this is particularly important on Travis CI, where
56 | # the timezone may not be the same as our dev machine
57 | os.environ['TZ'] = "PST-0800"
58 | time.tzset()
59 |
60 | def tearDown(self):
61 | sys.stdout = self.stdout
62 | sys.stderr = self.stderr
63 |
64 | def test_print_line(self):
65 | printLine('test')
66 | sys.stdout.seek(0)
67 | self.assertEqual(sys.stdout.read(), 'test\n')
68 |
69 | def test_print_line_other_endline_success(self):
70 | printLine('test', endLine='\n\r')
71 | sys.stdout.seek(0)
72 | self.assertEqual(sys.stdout.read(), 'test\n\r')
73 |
74 | def test_print_about_success(self):
75 | about = '''Version: %s
76 | Geeknote - a command line client for Evernote.
77 | Use geeknote --help to read documentation.\n''' % __version__
78 | printAbout()
79 | sys.stdout.seek(0)
80 | self.assertEqual(sys.stdout.read(), about)
81 |
82 | def test_separator_with_title_success(self):
83 | line = '------------------- test ------------------\n'
84 | separator(symbol='-', title='test')
85 | sys.stdout.seek(0)
86 | self.assertEqual(sys.stdout.read(), line)
87 |
88 | def test_separator_without_title_success(self):
89 | line = '----------------------------------------\n\n'
90 | separator(symbol='-')
91 | sys.stdout.seek(0)
92 | self.assertEqual(sys.stdout.read(), line)
93 |
94 | def test_separator_empty_args_success(self):
95 | separator()
96 | sys.stdout.seek(0)
97 | self.assertEqual(sys.stdout.read(), '\n\n')
98 |
99 | def test_failure_message_success(self):
100 | sav = sys.stderr
101 | buf = StringIO()
102 | sys.stderr = buf
103 | failureMessage('fail')
104 | sys.stderr = sav
105 | buf.seek(0)
106 | self.assertEqual(buf.read(), 'fail\n')
107 |
108 | def test_success_message_success(self):
109 | successMessage('success')
110 | sys.stdout.seek(0)
111 | self.assertEqual(sys.stdout.read(), 'success\n')
112 |
113 | def test_show_user_without_fullinfo_success(self):
114 | showUser(UserStub(), {})
115 | info = '''################ USER INFO ################
116 | Username : testusername
117 | Name : testname
118 | Email : testemail\n'''
119 | sys.stdout.seek(0)
120 | self.assertEqual(sys.stdout.read(), info)
121 |
122 | def test_show_user_with_fullinfo_success(self):
123 | showUser(UserStub(), True)
124 | info = '''################ USER INFO ################
125 | Username : testusername
126 | Name : testname
127 | Email : testemail
128 | Upload limit : 0.00 MB
129 | Upload limit end : 2004-09-17
130 | Timezone : None\n'''
131 | sys.stdout.seek(0)
132 | self.assertEqual(sys.stdout.read(), info)
133 |
134 | def test_show_note_success(self):
135 | note = '''################### URL ###################
136 | NoteLink: https://www.evernote.com/shard/222/nl/111/12345
137 | WebClientURL: https://www.evernote.com/Home.action?#n=12345
138 | ################## TITLE ##################
139 | testnote
140 | =================== META ==================
141 | Notebook: geeknotes
142 | Created: 2004-09-17
143 | Updated: 2004-09-17
144 | |||||||||||||||| REMINDERS ||||||||||||||||
145 | Order: None
146 | Time: None
147 | Done: None
148 | ----------------- CONTENT -----------------
149 | Tags: tag1, tag2, tag3
150 | ##note content\n\n\n'''
151 | showNote(NoteStub(), UserStub().id, UserStub().shardId)
152 | sys.stdout.seek(0)
153 | self.assertEqual(sys.stdout.read(), note)
154 |
155 | def test_print_list_without_title_success(self):
156 | notes_list = '''Found 2 items
157 | 1 : 2004-09-17 2004-09-17 testnote
158 | 2 : 2004-09-17 2004-09-17 testnote\n'''
159 | printList([NoteStub() for _ in range(2)])
160 | sys.stdout.seek(0)
161 | self.assertEqual(sys.stdout.read(), notes_list)
162 |
163 | def test_print_list_with_title_success(self):
164 | notes_list = '''=================== test ==================
165 | Found 2 items
166 | 1 : 2004-09-17 2004-09-17 testnote
167 | 2 : 2004-09-17 2004-09-17 testnote\n'''
168 | printList([NoteStub() for _ in range(2)], title='test')
169 | sys.stdout.seek(0)
170 | self.assertEqual(sys.stdout.read(), notes_list)
171 |
172 | def test_print_list_with_urls_success(self):
173 | notes_list = '''=================== test ==================
174 | Found 2 items
175 | 1 : 2004-09-17 2004-09-17 testnote >>> https://{url}/Home.action?#n=12345
176 | 2 : 2004-09-17 2004-09-17 testnote >>> https://{url}/Home.action?#n=12345
177 | '''.format(url=USER_BASE_URL)
178 | printList([NoteStub() for _ in range(2)], title='test', showUrl=True)
179 | sys.stdout.seek(0)
180 | self.assertEqual(sys.stdout.read(), notes_list)
181 |
182 | def test_print_list_with_selector_success(self):
183 | out.rawInput = lambda x: 2
184 | notes_list = '''=================== test ==================
185 | Found 2 items
186 | 1 : 2004-09-17 2004-09-17 testnote
187 | 2 : 2004-09-17 2004-09-17 testnote
188 | 0 : -Cancel-\n'''
189 | out.printList([NoteStub() for _ in range(2)], title='test', showSelector=True)
190 | sys.stdout.seek(0)
191 | self.assertEqual(sys.stdout.read(), notes_list)
192 |
193 | def test_search_result_success(self):
194 | result = '''Search request: test
195 | Found 2 items
196 | 1 : 2004-09-17 2004-09-17 testnote
197 | 2 : 2004-09-17 2004-09-17 testnote\n'''
198 | SearchResult([NoteStub() for _ in range(2)], 'test')
199 | sys.stdout.seek(0)
200 | self.assertEqual(sys.stdout.read(), result)
201 |
202 | def test_print_date(self):
203 | self.assertEqual(printDate(1095292800000), '2004-09-17')
204 |
--------------------------------------------------------------------------------
/tests/test_sandbox.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import unittest
3 | from geeknote import config
4 | from geeknote.geeknote import User, Notebooks, Notes, GeekNote, GeekNoteConnector
5 | from geeknote.storage import Storage
6 | from geeknote.oauth import GeekNoteAuth
7 | from random import SystemRandom
8 | from string import hexdigits
9 | if config.DEV_MODE:
10 | from proxyenv.proxyenv import ProxyFactory
11 |
12 |
13 | # see https://docs.python.org/2.7/library/unittest.html §25.3.6
14 | # http://thecodeship.com/patterns/guide-to-python-function-decorators/
15 | # (decorator with empty argument list)
16 | def skipUnlessDevMode():
17 | if config.DEV_MODE:
18 | return lambda x: x
19 | else:
20 | return unittest.skip("Test only active with DEV_MODE=True")
21 |
22 |
23 | class TestSandbox(unittest.TestCase):
24 |
25 | @classmethod
26 | @skipUnlessDevMode()
27 | def setUpClass(cls):
28 | storage = Storage()
29 |
30 | # start out with empty auth token. Save current token to restore it later.
31 | cls.token = storage.getUserToken()
32 | cls.info = storage.getUserInfo()
33 | storage.removeUser()
34 |
35 | # Force reconnection and re-authorization because it's part of our test suite
36 | GeekNoteAuth.cookies = {}
37 | GeekNoteConnector.evernote = None
38 | GeekNote.skipInitConnection = False
39 | cls.storage = Storage()
40 | cls.notes = set()
41 | cls.nbs = set()
42 | cls.notebook = ("Geeknote test %s please delete" %
43 | "".join(SystemRandom().choice(hexdigits) for x in range(12)))
44 | cls.Notes = Notes()
45 | cls.Notebooks = Notebooks()
46 | cls.Geeknote = cls.Notebooks.getEvernote()
47 |
48 | @classmethod
49 | def tearDownClass(cls):
50 | if cls.token:
51 | cls.storage.createUser(cls.token, cls.info)
52 |
53 | def setUp(self):
54 | self.user = User()
55 | self.tag = "geeknote_unittest_1"
56 |
57 | @skipUnlessDevMode()
58 | def test01_userLogin(self):
59 | # This is an implicit test. The GeekNote() call in setUp() will perform
60 | # an automatic login.
61 | self.assertTrue(self.Geeknote.checkAuth())
62 |
63 | @skipUnlessDevMode()
64 | def test10_createNotebook(self):
65 | self.assertTrue(self.Notebooks.create(self.notebook))
66 |
67 | @skipUnlessDevMode()
68 | def test15_findNotebook(self):
69 | all = self.Geeknote.findNotebooks()
70 | nb = [nb for nb in all if nb.name == self.notebook]
71 | self.assertTrue(len(nb) == 1)
72 | self.nbs.add(nb[0].guid)
73 |
74 | @skipUnlessDevMode()
75 | def test30_createNote(self):
76 | self.Notes.create("note title 01",
77 | content="""\
78 | # Sample note 01
79 | This is the note text.
80 | """,
81 | notebook=self.notebook,
82 | tags=self.tag)
83 |
84 | @skipUnlessDevMode()
85 | def test31_findNote(self):
86 | self.Notes.find(notebooks=self.notebook, tags=self.tag)
87 | result = self.storage.getSearch()
88 | self.assertTrue(len(result.notes) == 1)
89 | self.notes.add(result.notes[0].guid)
90 |
91 | @skipUnlessDevMode()
92 | def test90_removeNotes(self):
93 | while self.notes:
94 | self.assertTrue(self.Geeknote.removeNote(self.notes.pop()))
95 |
96 | # EXPECTED FAILURE
97 | # "This function is generally not available to third party applications"
98 | # https://dev.evernote.com/doc/reference/NoteStore.html#Fn_NoteStore_expungeNotebook
99 | @skipUnlessDevMode()
100 | def test95_removeNotebooks(self):
101 | while self.nbs:
102 | # self.assertTrue(self.Geeknote.removeNotebook(self.nbs.pop()))
103 | self.assertRaises(SystemExit, self.Geeknote.removeNotebook, self.nbs.pop())
104 |
105 | @skipUnlessDevMode()
106 | def test99_userLogout(self):
107 | self.user.logout(force=True)
108 | self.assertFalse(self.Geeknote.checkAuth())
109 |
110 |
111 | class TestSandboxWithProxy(TestSandbox):
112 |
113 | @classmethod
114 | @skipUnlessDevMode()
115 | def setUpClass(cls):
116 | cls.proxy = ProxyFactory()()
117 | cls.proxy.start()
118 | cls.proxy.wait()
119 | cls.proxy.enter_environment()
120 | super(TestSandboxWithProxy, cls).setUpClass()
121 |
122 | @classmethod
123 | def tearDownClass(cls):
124 | super(TestSandboxWithProxy, cls).tearDownClass()
125 | cls.proxy.leave_environment()
126 | try:
127 | cls.proxy.stop()
128 | except:
129 | pass
130 |
--------------------------------------------------------------------------------
/tests/test_storage.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import unittest
4 | from sqlalchemy.engine import create_engine
5 | from sqlalchemy.orm.session import sessionmaker
6 | from geeknote import storage
7 | import pickle
8 |
9 |
10 | def hacked_init(self):
11 | '''Hack for testing'''
12 | engine = create_engine('sqlite:///:memory:', echo=False)
13 | storage.Base.metadata.create_all(engine)
14 | Session = sessionmaker(bind=engine)
15 | self.session = Session()
16 |
17 |
18 | class storageTest(unittest.TestCase):
19 | def setUp(self):
20 | stor = storage.Storage
21 | stor.__init__ = hacked_init
22 | self.storage = stor()
23 | self.otoken = 'testoauthtoken'
24 | self.userinfo = {'email': 'test@mail.com'}
25 | self.tags = {'tag': 1, 'tag2': 2, 'tag3': 'lol'}
26 | self.notebooks = {'notebook': 'mylaptop'}
27 | self.storage.createUser(self.otoken,
28 | self.userinfo)
29 |
30 | def test_create_user_without_token_fail(self):
31 | self.assertFalse(self.storage.createUser(None, self.userinfo))
32 |
33 | def test_create_user_without_info_fail(self):
34 | self.assertFalse(self.storage.createUser(self.otoken, None))
35 |
36 | def test_remove_user_success(self):
37 | self.assertTrue(self.storage.removeUser())
38 |
39 | def test_get_user_token_success(self):
40 | self.assertEqual(self.storage.getUserToken(), self.otoken)
41 |
42 | def test_get_user_info_success(self):
43 | self.assertEqual(self.storage.getUserInfo(), self.userinfo)
44 |
45 | def test_get_user_props_success(self):
46 | props = [{'oAuthToken': 'testoauthtoken'},
47 | {'info': {'email': 'test@mail.com'}}]
48 | self.assertEqual(self.storage.getUserprops(), props)
49 |
50 | def test_get_user_props_exists_success(self):
51 | self.assertEqual(self.storage.getUserprop('info'),
52 | self.userinfo)
53 |
54 | def test_get_user_prop_not_exists(self):
55 | self.assertFalse(self.storage.getUserprop('some_prop'))
56 |
57 | def test_set_new_user_prop(self):
58 | self.assertFalse(self.storage.getUserprop('kkey'))
59 | self.assertTrue(self.storage.setUserprop('some_key', 'some_value'))
60 | self.assertEqual(self.storage.getUserprop('some_key'), 'some_value')
61 |
62 | def test_set_exists_user_prop(self):
63 | newmail = {'email': 'new_email@mail.com'}
64 | self.assertEqual(self.storage.getUserprop('info'), self.userinfo)
65 | self.assertTrue(self.storage.setUserprop('info', newmail), newmail)
66 | self.assertEqual(self.storage.getUserprop('info'), newmail)
67 |
68 | def test_get_empty_settings(self):
69 | self.assertEqual(self.storage.getSettings(), {})
70 |
71 | def test_set_settings_success(self):
72 | self.storage.setSettings({'editor': 'vim'})
73 | self.assertEqual(self.storage.getSettings(),
74 | {'editor': "vim"})
75 |
76 | def test_set_setting_error_type_fail(self):
77 | self.assertFalse(self.storage.setSettings('editor'))
78 |
79 | def test_set_setting_none_value_fail(self):
80 | self.assertFalse(self.storage.setSettings({'key': None}))
81 |
82 | def test_update_settings_fail(self):
83 | self.storage.setSettings({'editor': 'vim'})
84 | self.assertTrue(self.storage.setSettings({'editor': 'nano'}))
85 | self.assertEqual(self.storage.getSettings(),
86 | {'editor': 'nano'})
87 |
88 | def test_get_setting_exist_success(self):
89 | self.storage.setSettings({'editor': 'vim'})
90 | editor = self.storage.getSetting('editor')
91 | self.assertEqual(pickle.loads(editor), 'vim')
92 |
93 | def test_set_setting_true(self):
94 | editor = 'nano'
95 | self.assertTrue(self.storage.setSetting('editor', editor))
96 | self.assertEqual(self.storage.getSetting('editor'), editor)
97 |
98 | def test_get_setting_not_exist_fail(self):
99 | self.assertFalse(self.storage.getSetting('editor'))
100 |
101 | def test_set_tags_success(self):
102 | self.assertTrue(self.storage.setTags(self.tags))
103 |
104 | def test_set_tags_error_type_fail(self):
105 | self.assertFalse(self.storage.setTags('tag'))
106 |
107 | def test_set_tags_none_value_fail(self):
108 | self.assertFalse(self.storage.setTags({'tag': None}))
109 |
110 | def test_get_tags_success(self):
111 | tags = {'tag': '1', 'tag2': '2', 'tag3': 'lol'}
112 | self.assertTrue(self.storage.setTags(self.tags))
113 | self.assertEqual(self.storage.getTags(), tags)
114 |
115 | def test_replace_tags_success(self):
116 | tags = {'tag': '1', 'tag2': '2', 'tag3': '3'}
117 | self.assertTrue(self.storage.setTags(self.tags))
118 | self.tags['tag3'] = 3
119 | self.assertTrue(self.storage.setTags(self.tags))
120 | self.assertEqual(self.storage.getTags(), tags)
121 |
122 | def test_set_notebooks_success(self):
123 | self.assertEqual(self.storage.getNotebooks(), {})
124 | self.storage.setNotebooks(self.notebooks)
125 | self.assertEqual(self.storage.getNotebooks(), self.notebooks)
126 |
127 | def test_replace_notebooks_success(self):
128 | newnotebooks = {'notebook': 'android'}
129 | self.storage.setNotebooks(self.notebooks)
130 | self.storage.setNotebooks(newnotebooks)
131 | self.assertEqual(self.storage.getNotebooks(), newnotebooks)
132 |
133 | def test_get_empty_search_success(self):
134 | self.assertFalse(self.storage.getSearch())
135 |
136 | def test_get_search_exists_success(self):
137 | query = 'my query'
138 | self.assertTrue(self.storage.setSearch(query))
139 | self.assertEqual(self.storage.getSearch(), query)
140 |
141 | def test_set_notebooks_error_type_fail(self):
142 | self.assertFalse(self.storage.setNotebooks('book'))
143 |
144 | def test_set_notebooks_none_value_fail(self):
145 | self.assertFalse(self.storage.setNotebooks({'book': None}))
146 |
147 | def test_set_search_true(self):
148 | self.assertTrue(self.storage.setSearch('my query'))
149 |
150 |
151 | class modelsTest(unittest.TestCase):
152 | def test_rept_userprop(self):
153 | userprop = storage.Userprop(key='test',
154 | value='value')
155 | self.assertEqual(userprop.__repr__(),
156 | "")
157 |
158 | def test_repr_setting(self):
159 | setting = storage.Setting(key='test',
160 | value='value')
161 | self.assertEqual(setting.__repr__(),
162 | "")
163 |
164 | def test_repr_notebook(self):
165 | notebook = storage.Notebook(name='notebook',
166 | guid='testguid')
167 | self.assertEqual(notebook.__repr__(),
168 | "")
169 |
170 | def test_repr_tag(self):
171 | tag = storage.Tag(tag='testtag',
172 | guid='testguid')
173 | self.assertEqual(tag.__repr__(), "")
174 |
175 | def test_repr_search(self):
176 | search = storage.Search(search_obj='query')
177 | self.assertEqual(search.__repr__(),
178 | "" % search.timestamp)
179 |
--------------------------------------------------------------------------------
/tests/test_tools.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import unittest
4 | from geeknote.tools import checkIsInt, strip, decodeArgs,\
5 | stdinEncode, stdoutEncode, Struct
6 |
7 |
8 | class testTools(unittest.TestCase):
9 |
10 | def test_check_is_int_success(self):
11 | self.assertTrue(checkIsInt(1))
12 |
13 | def test_check_is_int_float_success(self):
14 | self.assertTrue(checkIsInt(1.1))
15 |
16 | def test_check_is_int_false(self):
17 | self.assertTrue(checkIsInt('1'))
18 |
19 | def test_strip_none_data_success(self):
20 | self.assertFalse(strip(None))
21 |
22 | def test_strip_dict_data_success(self):
23 | data = {'key \t\n\r\"\'': 'test'}
24 | self.assertEqual(strip(data), {'key': 'test'})
25 |
26 | def test_strip_list_data_success(self):
27 | data = ['key \t\n\r\"\'', 'value \t\n\r\"\'']
28 | self.assertEqual(strip(data), ['key', 'value'])
29 |
30 | def test_strip_str_data_success(self):
31 | data = 'text text text \t\n\r\"\''
32 | self.assertEqual(strip(data), 'text text text')
33 |
34 | def test_strip_int_data_false(self):
35 | self.assertRaises(Exception, strip, 1)
36 |
37 | def test_struct_success(self):
38 | struct = Struct(key='value')
39 | self.assertEqual(struct.__dict__, {'key': 'value'})
40 |
41 | # stdinEncode need mock for stdin to test if different encoding is used
42 | # def test_decode_args_success(self):
43 | # result = [1, '2', 'test', '\xc2\xae',
44 | # '\xd1\x82\xd0\xb5\xd1\x81\xd1\x82']
45 | # self.assertEqual(decodeArgs([1, '2', 'test', '®', 'тест']), result)
46 |
47 | # def test_stdinEncode_success(self):
48 | # self.assertEqual(stdinEncode('тест'), 'тест')
49 | # self.assertEqual(stdinEncode('test'), 'test')
50 | # self.assertEqual(stdinEncode('®'), '®')
51 | # self.assertEqual(stdinEncode(1), 1)
52 |
53 | # def test_stdoutEncode_success(self):
54 | # self.assertEqual(stdoutEncode('тест'), 'тест')
55 | # self.assertEqual(stdoutEncode('test'), 'test')
56 | # self.assertEqual(stdoutEncode('®'), '®')
57 | # self.assertEqual(stdoutEncode(1), 1)
58 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27
3 |
4 | [pep8]
5 | ignore = E501, E241
6 | max-line-length = 256
7 |
8 | [flake8]
9 | ignore = E501, E241
10 | max-line-length = 256
11 |
12 | [testenv]
13 | deps =
14 | pytest
15 | mock
16 | commands=py.test
17 |
--------------------------------------------------------------------------------