├── .gitignore ├── .idea ├── flashcards.iml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── dev ├── __init__.py ├── create_google_token.py └── tryout.py ├── docs ├── README.md ├── example.ipynb └── example2.ipynb ├── gflashcards ├── __init__.py ├── app.py ├── card.py ├── renderer │ ├── markdown-hot.css │ └── markdown-hot.js ├── tags.py ├── upload.py └── utils.py ├── pyproject.lock ├── pyproject.toml └── user └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,python,pycharm,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### PyCharm ### 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 22 | 23 | # User-specific stuff 24 | .idea/**/workspace.xml 25 | .idea/**/tasks.xml 26 | .idea/**/usage.statistics.xml 27 | .idea/**/dictionaries 28 | .idea/**/shelf 29 | 30 | # Sensitive or high-churn files 31 | .idea/**/dataSources/ 32 | .idea/**/dataSources.ids 33 | .idea/**/dataSources.local.xml 34 | .idea/**/sqlDataSources.xml 35 | .idea/**/dynamic.xml 36 | .idea/**/uiDesigner.xml 37 | .idea/**/dbnavigator.xml 38 | 39 | # Gradle 40 | .idea/**/gradle.xml 41 | .idea/**/libraries 42 | 43 | # Gradle and Maven with auto-import 44 | # When using Gradle or Maven with auto-import, you should exclude module files, 45 | # since they will be recreated, and may cause churn. Uncomment if using 46 | # auto-import. 47 | # .idea/modules.xml 48 | # .idea/*.iml 49 | # .idea/modules 50 | 51 | # CMake 52 | cmake-build-*/ 53 | 54 | # Mongo Explorer plugin 55 | .idea/**/mongoSettings.xml 56 | 57 | # File-based project format 58 | *.iws 59 | 60 | # IntelliJ 61 | out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Cursive Clojure plugin 70 | .idea/replstate.xml 71 | 72 | # Crashlytics plugin (for Android Studio and IntelliJ) 73 | com_crashlytics_export_strings.xml 74 | crashlytics.properties 75 | crashlytics-build.properties 76 | fabric.properties 77 | 78 | # Editor-based Rest Client 79 | .idea/httpRequests 80 | 81 | ### PyCharm Patch ### 82 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 83 | 84 | # *.iml 85 | # modules.xml 86 | # .idea/misc.xml 87 | # *.ipr 88 | 89 | # Sonarlint plugin 90 | .idea/sonarlint 91 | 92 | ### Python ### 93 | # Byte-compiled / optimized / DLL files 94 | __pycache__/ 95 | *.py[cod] 96 | *$py.class 97 | 98 | # C extensions 99 | *.so 100 | 101 | # Distribution / packaging 102 | .Python 103 | build/ 104 | develop-eggs/ 105 | dist/ 106 | downloads/ 107 | eggs/ 108 | .eggs/ 109 | lib/ 110 | lib64/ 111 | parts/ 112 | sdist/ 113 | var/ 114 | wheels/ 115 | *.egg-info/ 116 | .installed.cfg 117 | *.egg 118 | MANIFEST 119 | 120 | # PyInstaller 121 | # Usually these files are written by a python script from a template 122 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 123 | *.manifest 124 | *.spec 125 | 126 | # Installer logs 127 | pip-log.txt 128 | pip-delete-this-directory.txt 129 | 130 | # Unit test / coverage reports 131 | htmlcov/ 132 | .tox/ 133 | .coverage 134 | .coverage.* 135 | .cache 136 | nosetests.xml 137 | coverage.xml 138 | *.cover 139 | .hypothesis/ 140 | .pytest_cache/ 141 | 142 | # Translations 143 | *.mo 144 | *.pot 145 | 146 | # Django stuff: 147 | *.log 148 | local_settings.py 149 | db.sqlite3 150 | 151 | # Flask stuff: 152 | instance/ 153 | .webassets-cache 154 | 155 | # Scrapy stuff: 156 | .scrapy 157 | 158 | # Sphinx documentation 159 | docs/_build/ 160 | 161 | # PyBuilder 162 | target/ 163 | 164 | # Jupyter Notebook 165 | .ipynb_checkpoints 166 | 167 | # pyenv 168 | .python-version 169 | 170 | # celery beat schedule file 171 | celerybeat-schedule 172 | 173 | # SageMath parsed files 174 | *.sage.py 175 | 176 | # Environments 177 | .env 178 | .venv 179 | env/ 180 | venv/ 181 | ENV/ 182 | env.bak/ 183 | venv.bak/ 184 | 185 | # Spyder project settings 186 | .spyderproject 187 | .spyproject 188 | 189 | # Rope project settings 190 | .ropeproject 191 | 192 | # mkdocs documentation 193 | /site 194 | 195 | # mypy 196 | .mypy_cache/ 197 | 198 | ### Python Patch ### 199 | .venv/ 200 | 201 | ### Windows ### 202 | # Windows thumbnail cache files 203 | Thumbs.db 204 | ehthumbs.db 205 | ehthumbs_vista.db 206 | 207 | # Dump file 208 | *.stackdump 209 | 210 | # Folder config file 211 | [Dd]esktop.ini 212 | 213 | # Recycle Bin used on file shares 214 | $RECYCLE.BIN/ 215 | 216 | # Windows Installer files 217 | *.cab 218 | *.msi 219 | *.msix 220 | *.msm 221 | *.msp 222 | 223 | # Windows shortcuts 224 | *.lnk 225 | 226 | 227 | # End of https://www.gitignore.io/api/linux,python,pycharm,windows 228 | 229 | # User 230 | user.* 231 | -------------------------------------------------------------------------------- /.idea/flashcards.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gflashcards 2 | 3 | Google sheets-based flashcard maker 4 | 5 | ## Usage 6 | 7 | ```python 8 | >>> from gflashcards import Flashcard 9 | >>> fc = Flashcards(spreadsheet_id, sheet_name, path_to_token, path_to_clientsecrets) 10 | >>> card = fc.quiz(ham_regex, tags=[spam_regex, nam_regex]) 11 | >>> card 12 | The front side of the card is shown on Jupyter Notebook. Images included. Both Markdown and HTML works. 13 | >>> card.show() 14 | The back side of the card is shown. 15 | ``` 16 | 17 | The `path_to_token` and `path_to_clientsecrets` can be omitted, and default to `user/token.json` and `user/credentials.json`, respectively. 18 | 19 | `sheet_name` can be omitted and default to "flashcards". 20 | 21 | `spreadsheet_id` can be extracted from the URL of the Google Sheets. 22 | 23 | More information about `token`, `clientsecrets`, and `spreadsheet_id` can be viewed at https://developers.google.com/sheets/api/guides/concepts 24 | 25 | For more advanced usage, please see https://github.com/patarapolw/gflashcards/tree/master/docs 26 | 27 | ## Google Sheets formatting 28 | 29 | The first row is abandoned, but it should contain the header 'Front', 'Back', 'Keywords', and 'Tags', respectively. 30 | 31 | The Keywords and Tags are space-separated. If a keyword or a tag is longer than one word, it should be double-quoted. 32 | 33 | To make a new line in Google Sheets, type Alt+Enter. 34 | 35 | ## Known bugs 36 | 37 | You might have to run `dev/create_google_token.py` to get `token.json`. 38 | 39 | ## Offline version, for Excel 40 | 41 | Please see -- https://github.com/patarapolw/jupyter-flashcards 42 | 43 | ## Image from clipboard 44 | 45 | ```python 46 | >>> from gflashcards import save_image_from_clipboard 47 | >>> save_image_from_clipboard() 48 | The image will be shown on Jupyter Notebook, with Imgur image URL shown. 49 | ``` 50 | 51 | Dependencies: in Linux, `sudo apt install xclip`. 52 | 53 | ## Image from local 54 | 55 | The image from local also works (`from gflashcards import save_image_from_file`). The image will be uploaded to Imgur server. 56 | -------------------------------------------------------------------------------- /dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patarapolw/gflash/692e152bd803109f0bf8d0c22bff03d5c78bd70a/dev/__init__.py -------------------------------------------------------------------------------- /dev/create_google_token.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shows basic usage of the Sheets API. Prints values from a Google Spreadsheet. 3 | """ 4 | from __future__ import print_function 5 | from apiclient.discovery import build 6 | from httplib2 import Http 7 | from oauth2client import file, client, tools 8 | 9 | 10 | def create_token(secrets_path='../user/google/credentials.json', 11 | token_path='../user/google/token.json', 12 | scopes='https://www.googleapis.com/auth/spreadsheets.readonly'): 13 | # Setup the Sheets API 14 | store = file.Storage(token_path) 15 | creds = store.get() 16 | if not creds or creds.invalid: 17 | flow = client.flow_from_clientsecrets(secrets_path, scopes) 18 | creds = tools.run_flow(flow, store) 19 | service = build('sheets', 'v4', http=creds.authorize(Http())) 20 | 21 | return service 22 | 23 | 24 | if __name__ == '__main__': 25 | create_token() 26 | -------------------------------------------------------------------------------- /dev/tryout.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | if __name__ == '__main__': 5 | print(datetime.now().isoformat()) -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Advanced Usage 2 | 3 | ### `Flashcards` class 4 | 5 | #### `Flashcards.iter_quiz()` 6 | 7 | Create a generator for building a non-repeating quiz. 8 | 9 | ```python 10 | >>> from gflashcards import Flashcards 11 | >>> fc = Flashcards(spreadsheet_id) 12 | >>> iter_quiz = fc.iter_quiz(keyword_regex) 13 | >>> card = next(iter_quiz) 14 | ``` 15 | 16 | #### `Flashcards.view()` 17 | 18 | View what `Flashcards.quiz()` and `Flashcards.iter_quiz()` would possibly show. 19 | 20 | Also, you might actually learn more from a table, rather than from a flashcard. 21 | 22 | #### `Flashcards.tags` 23 | 24 | A property showing the list of all available tags. 25 | 26 | ## Uploading images to Imgur (programmatically) 27 | 28 | Please get a client-ID from https://api.imgur.com/oauth2/addclient 29 | 30 | Using [Postman](https://apidocs.imgur.com/) is the way recommended by Imgur. Postman is also available in Chrome browser. 31 | -------------------------------------------------------------------------------- /docs/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from gflashcards import Flashcards, save_image_from_clipboard" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "fc = Flashcards('1P0on84nEDMPeztFWhl9WMsj6BUGHBLh6GV5FU_jE7JM')" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 14, 24 | "metadata": {}, 25 | "outputs": [ 26 | { 27 | "data": { 28 | "text/html": [ 29 | "

" 30 | ], 31 | "text/plain": [ 32 | "" 33 | ] 34 | }, 35 | "execution_count": 14, 36 | "metadata": {}, 37 | "output_type": "execute_result" 38 | } 39 | ], 40 | "source": [ 41 | "card = fc.quiz(tags=['histo'])\n", 42 | "card" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 15, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "data": { 52 | "text/html": [ 53 | "

Card id: 189

Trabecular; carcinoid

Keywords:

Tags: histopathology

" 54 | ], 55 | "text/plain": [ 56 | "" 57 | ] 58 | }, 59 | "execution_count": 15, 60 | "metadata": {}, 61 | "output_type": "execute_result" 62 | } 63 | ], 64 | "source": [ 65 | "card.show()" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "metadata": {}, 72 | "outputs": [ 73 | { 74 | "data": { 75 | "text/html": [ 76 | "\n", 77 | " \n", 84 | " " 85 | ], 86 | "text/plain": [ 87 | "" 88 | ] 89 | }, 90 | "execution_count": 4, 91 | "metadata": {}, 92 | "output_type": "execute_result" 93 | } 94 | ], 95 | "source": [ 96 | "fc.view(tags=['marker'])" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [] 105 | } 106 | ], 107 | "metadata": { 108 | "kernelspec": { 109 | "display_name": "Python 3", 110 | "language": "python", 111 | "name": "python3" 112 | }, 113 | "language_info": { 114 | "codemirror_mode": { 115 | "name": "ipython", 116 | "version": 3 117 | }, 118 | "file_extension": ".py", 119 | "mimetype": "text/x-python", 120 | "name": "python", 121 | "nbconvert_exporter": "python", 122 | "pygments_lexer": "ipython3", 123 | "version": "3.7.0" 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 1 128 | } 129 | -------------------------------------------------------------------------------- /docs/example2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from gflashcards import Flashcards, save_image_from_clipboard" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": 2, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "fc = Flashcards('1P0on84nEDMPeztFWhl9WMsj6BUGHBLh6GV5FU_jE7JM')" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 17, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "iter_card = fc.iter_quiz(tags=['robbin'])" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 32, 33 | "metadata": {}, 34 | "outputs": [ 35 | { 36 | "data": { 37 | "text/html": [ 38 | "

Organellar biogenesis

" 39 | ], 40 | "text/plain": [ 41 | "" 42 | ] 43 | }, 44 | "execution_count": 32, 45 | "metadata": {}, 46 | "output_type": "execute_result" 47 | } 48 | ], 49 | "source": [ 50 | "card = next(iter_card)\n", 51 | "card" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 33, 57 | "metadata": {}, 58 | "outputs": [ 59 | { 60 | "data": { 61 | "text/html": [ 62 | "

Card id: 222

Replication of organelles

Keywords:

Tags: Robbins chapter 1

" 63 | ], 64 | "text/plain": [ 65 | "" 66 | ] 67 | }, 68 | "execution_count": 33, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "card.show()" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 5, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "data": { 84 | "text/html": [ 85 | "\n", 86 | " \n", 93 | " " 94 | ], 95 | "text/plain": [ 96 | "" 97 | ] 98 | }, 99 | "execution_count": 5, 100 | "metadata": {}, 101 | "output_type": "execute_result" 102 | } 103 | ], 104 | "source": [ 105 | "fc.view(tags=['marker'])" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": null, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.7.0" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 1 137 | } 138 | -------------------------------------------------------------------------------- /gflashcards/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import Flashcards 2 | from .upload import save_image_from_clipboard, save_image_from_file 3 | -------------------------------------------------------------------------------- /gflashcards/app.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | from pyhandsontable import view_table 4 | from threading import Timer 5 | from pathlib import Path 6 | from bs4 import BeautifulSoup 7 | 8 | from apiclient.discovery import build 9 | from httplib2 import Http 10 | from oauth2client import file, client, tools 11 | 12 | from .tags import tag_reader 13 | from .utils import get_url_images_in_text 14 | from .card import CardQuiz, CardTuple 15 | from .utils import compare_list_match_regex 16 | 17 | 18 | class Flashcards: 19 | SCOPES = 'https://www.googleapis.com/auth/spreadsheets.readonly' 20 | 21 | def __init__(self, spreadsheet_id: str, sheet_name: str= 'flashcards', clientsecrets_path=None, token_path=None): 22 | """ 23 | :param str spreadsheet_id: Google Sheets spreadsheet id 24 | :param str sheet_name: Google Sheets sheet_name 25 | :param str|Path clientsecrets_path: 26 | :param str|Path token_path: 27 | """ 28 | range = '{}!A2:D'.format(sheet_name) 29 | 30 | if clientsecrets_path is None: 31 | clientsecrets_path = Path('user/google/credentials.json') 32 | if not clientsecrets_path.parent.exists(): 33 | clientsecrets_path.parent.mkdir() 34 | if token_path is None: 35 | token_path = clientsecrets_path.with_name('token.json') 36 | 37 | store = file.Storage(str(token_path)) 38 | creds = store.get() 39 | if not creds or creds.invalid: 40 | flow = client.flow_from_clientsecrets(str(clientsecrets_path), self.SCOPES) 41 | creds = tools.run_flow(flow, store) 42 | service = build('sheets', 'v4', http=creds.authorize(Http())) 43 | 44 | # Call the Sheets API 45 | result = service.spreadsheets().values().get(spreadsheetId=spreadsheet_id, 46 | range=range).execute() 47 | values = result.get('values', []) 48 | self.data = list() 49 | if not values: 50 | print('No data found.') 51 | else: 52 | for row in values: 53 | self.data.append(CardTuple(*row)) 54 | 55 | def find(self, keyword_regex: str = '', tags=None): 56 | if tags is None: 57 | tags = list() 58 | elif isinstance(tags, str): 59 | tags = [tags] 60 | else: 61 | tags = tags 62 | 63 | matched_entries = set() 64 | for i, item in enumerate(self.data): 65 | keywords = tag_reader(item.keywords) 66 | keywords.add(item.front) 67 | keywords.add(item.back) 68 | 69 | for keyword in keywords: 70 | if re.search(keyword_regex, keyword, flags=re.IGNORECASE): 71 | matched_entries.add(i) 72 | 73 | for i in matched_entries: 74 | if len(tags) == 0: 75 | yield i, self.data[i] 76 | elif compare_list_match_regex(tags, tag_reader(self.data[i].tags)): 77 | yield i, self.data[i] 78 | 79 | def view(self, keyword_regex: str='', tags: list=None, width=800, height=500): 80 | renderers = { 81 | 1: 'markdownRenderer', 82 | 2: 'markdownRenderer' 83 | } 84 | config = { 85 | 'colHeaders': ['id'] + list(CardTuple._fields), 86 | 'rowHeaders': False 87 | } 88 | 89 | filename = Path('temp.handsontable.html') 90 | try: 91 | table = view_table(data=list(reversed([[i] + list(record.to_formatted_tuple()) 92 | for i, record in self.find(keyword_regex, tags)])), 93 | width=width, 94 | height=height, 95 | renderers=renderers, 96 | config=config, 97 | filename=str(filename), 98 | autodelete=False) 99 | with filename.open('r') as f: 100 | soup = BeautifulSoup(f.read(), 'html5lib') 101 | 102 | style = soup.new_tag('style') 103 | 104 | with Path('gflashcards/renderer/markdown-hot.css').open('r') as f: 105 | style.append(f.read()) 106 | 107 | soup.head.append(style) 108 | 109 | div = soup.new_tag('div') 110 | 111 | js_markdown = soup.new_tag('script', 112 | src='https://cdn.rawgit.com/showdownjs/showdown/1.8.6/dist/showdown.min.js') 113 | js_custom = soup.new_tag('script') 114 | 115 | with Path('gflashcards/renderer/markdown-hot.js').open('r') as f: 116 | js_custom.append(f.read()) 117 | 118 | div.append(js_markdown) 119 | div.append(js_custom) 120 | 121 | script_tag = soup.find('script', {'id': 'generateHandsontable'}) 122 | soup.body.insert(soup.body.contents.index(script_tag), div) 123 | 124 | with filename.open('w') as f: 125 | f.write(str(soup)) 126 | 127 | return table 128 | finally: 129 | Timer(5, filename.unlink).start() 130 | # pass 131 | 132 | def iter_quiz(self, keyword_regex: str='', tags: list=None, exclude: list =None, image_only=False, 133 | begin:int=None, last: int=None): 134 | if exclude is None: 135 | exclude = list() 136 | 137 | all_records = list(reversed([(i, record) 138 | for i, record in self.find(keyword_regex, tags) 139 | if i not in exclude]))[begin:last] 140 | 141 | if image_only: 142 | all_records = [(i, record) for i, record in all_records 143 | if len(get_url_images_in_text(record.front)) > 0] 144 | 145 | if len(all_records) == 0: 146 | return "There is no record matching the criteria." 147 | 148 | random.shuffle(all_records) 149 | for i, record in all_records: 150 | yield CardQuiz(i, record) 151 | 152 | def quiz(self, keyword_regex: str='', tags: list=None, exclude: list =None, image_only=False, 153 | begin: int=None, last: int=None): 154 | return next(self.iter_quiz(keyword_regex, tags, exclude, image_only, begin=begin, last=last)) 155 | 156 | @property 157 | def tags(self): 158 | tags = set() 159 | 160 | for v in self.data: 161 | tags.update(tag_reader(v.tags)) 162 | 163 | return tags 164 | -------------------------------------------------------------------------------- /gflashcards/card.py: -------------------------------------------------------------------------------- 1 | from markdown import markdown 2 | from IPython.display import HTML 3 | from typing import NamedTuple 4 | 5 | from .tags import tag_reader 6 | from .utils import parse_markdown 7 | 8 | 9 | class CardQuiz: 10 | def __init__(self, card_id, record): 11 | """ 12 | :param int card_id: 13 | :param dict|OrderedDict record: 14 | """ 15 | assert isinstance(record, CardTuple) 16 | 17 | self.record = record 18 | self.id = card_id 19 | 20 | def _repr_html_(self): 21 | html = parse_markdown(self.record.front) 22 | 23 | return html 24 | 25 | def show(self): 26 | html = markdown("**Card id:** {}".format(self.id)) 27 | html += parse_markdown(self.record.back) 28 | html += markdown("**Keywords:** " + ', '.join(tag_reader(self.record.keywords))) 29 | html += markdown("**Tags:** " + ', '.join(tag_reader(self.record.tags))) 30 | 31 | return HTML(html) 32 | 33 | 34 | class CardTuple(NamedTuple): 35 | front: str = '' 36 | back: str = '' 37 | keywords: str = '' 38 | tags: str = '' 39 | 40 | def to_formatted_tuple(self): 41 | return self.front, self.back, self.keywords, self.tags 42 | -------------------------------------------------------------------------------- /gflashcards/renderer/markdown-hot.css: -------------------------------------------------------------------------------- 1 | p { 2 | margin: 0em; 3 | } 4 | ul { 5 | margin-top: -1em; 6 | margin-bottom: -1em; 7 | } 8 | li { 9 | margin: -0.5em; 10 | } 11 | -------------------------------------------------------------------------------- /gflashcards/renderer/markdown-hot.js: -------------------------------------------------------------------------------- 1 | var img_regex = /(?:(?=^)|(?=\s).|^)([^\s<>"\']+\.(?:png|jpg|jpeg|gif))/g; 2 | var markdownConverter = new showdown.Converter; 3 | 4 | (function(Handsontable){ 5 | function customRenderer(hotInstance, td, row, column, prop, value, cellProperties) { 6 | var text = Handsontable.helper.stringify(value); 7 | text = text.replace(/\n+/g, "\n\n"); 8 | // text = text.replace('\n', "
"); 9 | text = text.replace(img_regex, 10 | ""); 11 | 12 | td.innerHTML = markdownConverter.makeHtml(text); 13 | // td.innerHTML = text; 14 | 15 | return td; 16 | } 17 | 18 | // Register an alias 19 | Handsontable.renderers.registerRenderer('markdownRenderer', customRenderer); 20 | 21 | })(Handsontable); 22 | -------------------------------------------------------------------------------- /gflashcards/tags.py: -------------------------------------------------------------------------------- 1 | def tag_reader(raw_tags: str): 2 | """ 3 | :param str raw_tags: 4 | :return list: 5 | >>> tag_reader('presenilin-1 presenilin-2') 6 | {'presenilin-1', 'presenilin-2'} 7 | >>> tag_reader('“Bouchard microaneurysms”') 8 | {'Bouchard microaneurysms'} 9 | >>> tag_reader('astrocytoma “Rosenthal fibers”') 10 | {'astrocytoma', 'Rosenthal fibers'} 11 | >>> tag_reader('“Frontotemporal dementia” TDP-43') 12 | {'Frontotemporal dementia', 'TDP-43'} 13 | """ 14 | output = set() 15 | tag = '' 16 | do_purge = True 17 | for char in raw_tags: 18 | if char == '“': 19 | do_purge = False 20 | elif char == '”': 21 | do_purge = True 22 | elif char == '\"': 23 | do_purge = not do_purge 24 | elif char == ' ' and do_purge: 25 | output.add(tag) 26 | tag = '' 27 | else: 28 | tag += char 29 | 30 | if tag != '': 31 | output.add(tag) 32 | 33 | return output 34 | 35 | 36 | def to_raw_tags(tags: iter): 37 | """ 38 | :param iter tags: 39 | :return str: 40 | >>> to_raw_tags(['presenilin-1', 'presenilin-2']) 41 | 'presenilin-1 presenilin-2' 42 | >>> to_raw_tags(['Bouchard microaneurysms']) 43 | '“Bouchard microaneurysms”' 44 | >>> to_raw_tags(['astrocytoma', 'Rosenthal fibers']) 45 | 'astrocytoma “Rosenthal fibers”' 46 | >>> to_raw_tags(['Frontotemporal dementia', 'TDP-43']) 47 | '“Frontotemporal dementia” TDP-43' 48 | """ 49 | if tags is None: 50 | tags = list() 51 | 52 | if isinstance(tags, str): 53 | tags = tags.split(' ') 54 | 55 | formatted_tags = set() 56 | for entry in set(tags): 57 | if ' ' in entry or '\"' in entry: 58 | entry = '“{}”'.format(entry.replace('\"', '\\\"')) 59 | formatted_tags.add(entry) 60 | 61 | return ' '.join(formatted_tags) 62 | -------------------------------------------------------------------------------- /gflashcards/upload.py: -------------------------------------------------------------------------------- 1 | from IPython.display import Image 2 | import requests 3 | import json 4 | from pathlib import Path 5 | 6 | import sys 7 | try: 8 | from PIL import ImageGrab 9 | except ImportError: 10 | import subprocess 11 | 12 | 13 | with open('user/imgur.json') as f: 14 | imgur = json.load(f) 15 | 16 | 17 | def save_image_from_clipboard(): 18 | filename = 'clipboard.png' 19 | 20 | if sys.platform in ["win32", "darwin"]: 21 | im_data = ImageGrab.grabclipboard() 22 | im_data.save(filename) 23 | else: 24 | with open(filename, 'wb') as f: 25 | subprocess.call(['xclip', '-se', 'c', '-t', 'image/png', '-o'], stdout=f) 26 | 27 | return save_image_from_file(filename) 28 | 29 | 30 | def save_image_from_file(filename: str): 31 | headers = { 32 | 'Authorization': 'Client-ID {}'.format(imgur['Client-ID']) 33 | } 34 | data = { 35 | 'image': Path(filename).read_bytes() 36 | } 37 | r = requests.post('https://api.imgur.com/3/image', headers=headers, data=data) 38 | 39 | img_url = r.json()['data']['link'] 40 | print(img_url) 41 | 42 | return Image(img_url) 43 | -------------------------------------------------------------------------------- /gflashcards/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from markdown import markdown 3 | 4 | 5 | def get_url_images_in_text(text): 6 | return re.findall(r'((?:(?<=^)|(?<=\s))[^\s<>\"\']+\.(?:png|jpg|jpeg|gif))', text) 7 | 8 | 9 | def compare_list_match_regex(subset, superset): 10 | def _sub_compare(): 11 | for super_item in superset: 12 | if re.search(sub_item, super_item, flags=re.IGNORECASE): 13 | return True 14 | 15 | return False 16 | 17 | result = [] 18 | for sub_item in subset: 19 | result.append(_sub_compare()) 20 | 21 | return all(result) 22 | 23 | 24 | def parse_markdown(text, image_width=500): 25 | text = re.sub(r'\n+', '\n\n', text) 26 | for url in get_url_images_in_text(text): 27 | text = text.replace(url, ''.format(url, image_width)) 28 | 29 | return markdown(text) 30 | -------------------------------------------------------------------------------- /pyproject.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "main" 3 | description = "Disable App Nap on OS X 10.9" 4 | name = "appnope" 5 | optional = false 6 | platform = "UNKNOWN" 7 | python-versions = "*" 8 | version = "0.1.0" 9 | 10 | [package.requirements] 11 | platform = "darwin" 12 | 13 | [[package]] 14 | category = "main" 15 | description = "Specifications for callback functions passed in to an API" 16 | name = "backcall" 17 | optional = false 18 | platform = "UNKNOWN" 19 | python-versions = "*" 20 | version = "0.1.0" 21 | 22 | [[package]] 23 | category = "main" 24 | description = "Screen-scraping library" 25 | name = "beautifulsoup4" 26 | optional = false 27 | platform = "UNKNOWN" 28 | python-versions = "*" 29 | version = "4.6.0" 30 | 31 | [[package]] 32 | category = "main" 33 | description = "An easy safelist-based HTML-sanitizing tool." 34 | name = "bleach" 35 | optional = false 36 | platform = "*" 37 | python-versions = "*" 38 | version = "2.1.3" 39 | 40 | [package.dependencies] 41 | html5lib = ">=0.99999999pre,<1.0b1 || >1.0b1,<1.0b2 || >1.0b2,<1.0b3 || >1.0b3,<1.0b4 || >1.0b4,<1.0b5 || >1.0b5,<1.0b6 || >1.0b6,<1.0b7 || >1.0b7,<1.0b8 || >1.0b8" 42 | six = "*" 43 | 44 | [[package]] 45 | category = "main" 46 | description = "Dummy package for Beautiful Soup" 47 | name = "bs4" 48 | optional = false 49 | platform = "*" 50 | python-versions = "*" 51 | version = "0.0.1" 52 | 53 | [package.dependencies] 54 | beautifulsoup4 = "*" 55 | 56 | [[package]] 57 | category = "main" 58 | description = "Extensible memoizing collections and decorators" 59 | name = "cachetools" 60 | optional = false 61 | platform = "*" 62 | python-versions = "*" 63 | version = "2.1.0" 64 | 65 | [[package]] 66 | category = "main" 67 | description = "Python package for providing Mozilla's CA Bundle." 68 | name = "certifi" 69 | optional = false 70 | platform = "*" 71 | python-versions = "*" 72 | version = "2018.4.16" 73 | 74 | [[package]] 75 | category = "main" 76 | description = "Universal encoding detector for Python 2 and 3" 77 | name = "chardet" 78 | optional = false 79 | platform = "*" 80 | python-versions = "*" 81 | version = "3.0.4" 82 | 83 | [[package]] 84 | category = "main" 85 | description = "Cross-platform colored terminal text." 86 | name = "colorama" 87 | optional = false 88 | platform = "UNKNOWN" 89 | python-versions = "*" 90 | version = "0.3.9" 91 | 92 | [package.requirements] 93 | platform = "win32" 94 | 95 | [[package]] 96 | category = "main" 97 | description = "Better living through Python with decorators" 98 | name = "decorator" 99 | optional = false 100 | platform = "All" 101 | python-versions = "*" 102 | version = "4.3.0" 103 | 104 | [[package]] 105 | category = "main" 106 | description = "Discover and load entry points from installed packages." 107 | name = "entrypoints" 108 | optional = false 109 | platform = "*" 110 | python-versions = ">=2.7" 111 | version = "0.2.3" 112 | 113 | [[package]] 114 | category = "main" 115 | description = "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" 116 | name = "enum34" 117 | optional = false 118 | platform = "UNKNOWN" 119 | python-versions = "*" 120 | version = "1.1.6" 121 | 122 | [package.requirements] 123 | python = ">=3.3,<3.4" 124 | 125 | [[package]] 126 | category = "main" 127 | description = "Google API Client Library for Python" 128 | name = "google-api-python-client" 129 | optional = false 130 | platform = "*" 131 | python-versions = "*" 132 | version = "1.7.4" 133 | 134 | [package.dependencies] 135 | google-auth = ">=1.4.1" 136 | google-auth-httplib2 = ">=0.0.3" 137 | httplib2 = ">=0.9.2,<1dev" 138 | six = ">=1.6.1,<2dev" 139 | uritemplate = ">=3.0.0,<4dev" 140 | 141 | [[package]] 142 | category = "main" 143 | description = "Google Authentication Library" 144 | name = "google-auth" 145 | optional = false 146 | platform = "*" 147 | python-versions = "*" 148 | version = "1.5.0" 149 | 150 | [package.dependencies] 151 | cachetools = ">=2.0.0" 152 | pyasn1-modules = ">=0.2.1" 153 | rsa = ">=3.1.4" 154 | six = ">=1.9.0" 155 | 156 | [[package]] 157 | category = "main" 158 | description = "Google Authentication Library: httplib2 transport" 159 | name = "google-auth-httplib2" 160 | optional = false 161 | platform = "*" 162 | python-versions = "*" 163 | version = "0.0.3" 164 | 165 | [package.dependencies] 166 | google-auth = "*" 167 | httplib2 = ">=0.9.1" 168 | 169 | [[package]] 170 | category = "main" 171 | description = "HTML parser based on the WHATWG HTML specification" 172 | name = "html5lib" 173 | optional = false 174 | platform = "*" 175 | python-versions = "*" 176 | version = "1.0.1" 177 | 178 | [package.dependencies] 179 | six = ">=1.9" 180 | webencodings = "*" 181 | 182 | [[package]] 183 | category = "main" 184 | description = "A comprehensive HTTP client library." 185 | name = "httplib2" 186 | optional = false 187 | platform = "*" 188 | python-versions = "*" 189 | version = "0.11.3" 190 | 191 | [[package]] 192 | category = "main" 193 | description = "Internationalized Domain Names in Applications (IDNA)" 194 | name = "idna" 195 | optional = false 196 | platform = "*" 197 | python-versions = "*" 198 | version = "2.7" 199 | 200 | [[package]] 201 | category = "main" 202 | description = "IPython Kernel for Jupyter" 203 | name = "ipykernel" 204 | optional = false 205 | platform = "Linux" 206 | python-versions = "*" 207 | version = "4.8.2" 208 | 209 | [package.dependencies] 210 | ipython = ">=4.0.0" 211 | jupyter-client = "*" 212 | tornado = ">=4.0" 213 | traitlets = ">=4.1.0" 214 | 215 | [[package]] 216 | category = "main" 217 | description = "IPython: Productive Interactive Computing" 218 | name = "ipython" 219 | optional = false 220 | platform = "Linux" 221 | python-versions = ">=3.3" 222 | version = "6.4.0" 223 | 224 | [package.dependencies] 225 | backcall = "*" 226 | decorator = "*" 227 | jedi = ">=0.10" 228 | pickleshare = "*" 229 | prompt-toolkit = ">=1.0.15,<2.0.0" 230 | pygments = "*" 231 | setuptools = ">=18.5" 232 | simplegeneric = ">0.8" 233 | traitlets = ">=4.2" 234 | 235 | [package.dependencies.appnope] 236 | platform = "darwin" 237 | version = "*" 238 | 239 | [package.dependencies.colorama] 240 | platform = "win32" 241 | version = "*" 242 | 243 | [package.dependencies.pathlib2] 244 | python = ">=3.3,<3.4" 245 | version = "*" 246 | 247 | [package.dependencies.pexpect] 248 | platform = "!=win32" 249 | version = "*" 250 | 251 | [package.dependencies.typing] 252 | python = "<=3.4" 253 | version = "*" 254 | 255 | [package.dependencies.win-unicode-console] 256 | platform = "win32" 257 | python = "<3.6" 258 | version = ">=0.5" 259 | 260 | [[package]] 261 | category = "main" 262 | description = "Vestigial utilities from IPython" 263 | name = "ipython-genutils" 264 | optional = false 265 | platform = "Linux" 266 | python-versions = "*" 267 | version = "0.2.0" 268 | 269 | [[package]] 270 | category = "main" 271 | description = "An autocompletion tool for Python that can be used for text editors." 272 | name = "jedi" 273 | optional = false 274 | platform = "any" 275 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 276 | version = "0.12.1" 277 | 278 | [package.dependencies] 279 | parso = ">=0.3.0" 280 | 281 | [[package]] 282 | category = "main" 283 | description = "A small but fast and easy to use stand-alone template engine written in pure python." 284 | name = "jinja2" 285 | optional = false 286 | platform = "*" 287 | python-versions = "*" 288 | version = "2.10" 289 | 290 | [package.dependencies] 291 | MarkupSafe = ">=0.23" 292 | 293 | [[package]] 294 | category = "main" 295 | description = "An implementation of JSON Schema validation for Python" 296 | name = "jsonschema" 297 | optional = false 298 | platform = "*" 299 | python-versions = "*" 300 | version = "2.6.0" 301 | 302 | [[package]] 303 | category = "main" 304 | description = "Jupyter metapackage. Install all the Jupyter components in one go." 305 | name = "jupyter" 306 | optional = false 307 | platform = "UNKNOWN" 308 | python-versions = "*" 309 | version = "1.0.0" 310 | 311 | [[package]] 312 | category = "main" 313 | description = "Jupyter protocol implementation and client libraries" 314 | name = "jupyter-client" 315 | optional = false 316 | platform = "Linux" 317 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 318 | version = "5.2.3" 319 | 320 | [package.dependencies] 321 | jupyter-core = "*" 322 | python-dateutil = ">=2.1" 323 | pyzmq = ">=13" 324 | tornado = ">=4.1" 325 | traitlets = "*" 326 | 327 | [[package]] 328 | category = "main" 329 | description = "Jupyter core package. A base package on which Jupyter projects rely." 330 | name = "jupyter-core" 331 | optional = false 332 | platform = "*" 333 | python-versions = "*" 334 | version = "4.4.0" 335 | 336 | [package.dependencies] 337 | traitlets = "*" 338 | 339 | [[package]] 340 | category = "main" 341 | description = "Python implementation of Markdown." 342 | name = "markdown" 343 | optional = false 344 | platform = "*" 345 | python-versions = "*" 346 | version = "2.6.11" 347 | 348 | [[package]] 349 | category = "main" 350 | description = "Implements a XML/HTML/XHTML Markup safe string for Python" 351 | name = "markupsafe" 352 | optional = false 353 | platform = "UNKNOWN" 354 | python-versions = "*" 355 | version = "1.0" 356 | 357 | [[package]] 358 | category = "main" 359 | description = "The fastest markdown parser in pure Python" 360 | name = "mistune" 361 | optional = false 362 | platform = "any" 363 | python-versions = "*" 364 | version = "0.8.3" 365 | 366 | [[package]] 367 | category = "main" 368 | description = "Converting Jupyter Notebooks" 369 | name = "nbconvert" 370 | optional = false 371 | platform = "Linux" 372 | python-versions = "*" 373 | version = "5.3.1" 374 | 375 | [package.dependencies] 376 | bleach = "*" 377 | entrypoints = ">=0.2.2" 378 | jinja2 = "*" 379 | jupyter-core = "*" 380 | mistune = ">=0.7.4" 381 | nbformat = ">=4.4" 382 | pandocfilters = ">=1.4.1" 383 | pygments = "*" 384 | testpath = "*" 385 | traitlets = ">=4.2" 386 | 387 | [[package]] 388 | category = "main" 389 | description = "The Jupyter Notebook format" 390 | name = "nbformat" 391 | optional = false 392 | platform = "Linux" 393 | python-versions = "*" 394 | version = "4.4.0" 395 | 396 | [package.dependencies] 397 | ipython-genutils = "*" 398 | jsonschema = ">=2.4,<2.5.0 || >2.5.0" 399 | jupyter-core = "*" 400 | traitlets = ">=4.1" 401 | 402 | [[package]] 403 | category = "main" 404 | description = "A web-based notebook environment for interactive computing" 405 | name = "notebook" 406 | optional = false 407 | platform = "Linux" 408 | python-versions = "*" 409 | version = "5.6.0" 410 | 411 | [package.dependencies] 412 | Send2Trash = "*" 413 | ipykernel = "*" 414 | ipython-genutils = "*" 415 | jinja2 = "*" 416 | jupyter-client = ">=5.2.0" 417 | jupyter-core = ">=4.4.0" 418 | nbconvert = "*" 419 | nbformat = "*" 420 | prometheus-client = "*" 421 | pyzmq = ">=17" 422 | terminado = ">=0.8.1" 423 | tornado = ">=4" 424 | traitlets = ">=4.2.1" 425 | 426 | [[package]] 427 | category = "main" 428 | description = "OAuth 2.0 client library" 429 | name = "oauth2client" 430 | optional = false 431 | platform = "*" 432 | python-versions = "*" 433 | version = "4.1.2" 434 | 435 | [package.dependencies] 436 | httplib2 = ">=0.9.1" 437 | pyasn1 = ">=0.1.7" 438 | pyasn1-modules = ">=0.0.5" 439 | rsa = ">=3.1.4" 440 | six = ">=1.6.1" 441 | 442 | [[package]] 443 | category = "main" 444 | description = "Utilities for writing pandoc filters in python" 445 | name = "pandocfilters" 446 | optional = false 447 | platform = "*" 448 | python-versions = "*" 449 | version = "1.4.2" 450 | 451 | [[package]] 452 | category = "main" 453 | description = "A Python Parser" 454 | name = "parso" 455 | optional = false 456 | platform = "any" 457 | python-versions = "*" 458 | version = "0.3.1" 459 | 460 | [[package]] 461 | category = "main" 462 | description = "Object-oriented filesystem paths" 463 | name = "pathlib2" 464 | optional = false 465 | platform = "*" 466 | python-versions = "*" 467 | version = "2.3.2" 468 | 469 | [package.dependencies] 470 | six = "*" 471 | 472 | [package.dependencies.scandir] 473 | python = "<3.5" 474 | version = "*" 475 | 476 | [package.requirements] 477 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 478 | 479 | [[package]] 480 | category = "main" 481 | description = "Pexpect allows easy control of interactive console applications." 482 | name = "pexpect" 483 | optional = false 484 | platform = "UNIX" 485 | python-versions = "*" 486 | version = "4.6.0" 487 | 488 | [package.dependencies] 489 | ptyprocess = ">=0.5" 490 | 491 | [package.requirements] 492 | platform = "!=win32" 493 | 494 | [[package]] 495 | category = "main" 496 | description = "Tiny 'shelve'-like database with concurrency support" 497 | name = "pickleshare" 498 | optional = false 499 | platform = "*" 500 | python-versions = "*" 501 | version = "0.7.4" 502 | 503 | [package.dependencies] 504 | 505 | [package.dependencies.pathlib2] 506 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 507 | version = "*" 508 | 509 | [[package]] 510 | category = "main" 511 | description = "Python Imaging Library (Fork)" 512 | name = "pillow" 513 | optional = false 514 | platform = "*" 515 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 516 | version = "5.2.0" 517 | 518 | [[package]] 519 | category = "main" 520 | description = "Python client for the Prometheus monitoring system." 521 | name = "prometheus-client" 522 | optional = false 523 | platform = "*" 524 | python-versions = "*" 525 | version = "0.3.0" 526 | 527 | [[package]] 528 | category = "main" 529 | description = "Library for building powerful interactive command lines in Python" 530 | name = "prompt-toolkit" 531 | optional = false 532 | platform = "*" 533 | python-versions = "*" 534 | version = "1.0.15" 535 | 536 | [package.dependencies] 537 | six = ">=1.9.0" 538 | wcwidth = "*" 539 | 540 | [[package]] 541 | category = "main" 542 | description = "Run a subprocess in a pseudo terminal" 543 | name = "ptyprocess" 544 | optional = false 545 | platform = "*" 546 | python-versions = "*" 547 | version = "0.6.0" 548 | 549 | [[package]] 550 | category = "main" 551 | description = "ASN.1 types and codecs" 552 | name = "pyasn1" 553 | optional = false 554 | platform = "any" 555 | python-versions = "*" 556 | version = "0.4.3" 557 | 558 | [[package]] 559 | category = "main" 560 | description = "A collection of ASN.1-based protocols modules." 561 | name = "pyasn1-modules" 562 | optional = false 563 | platform = "any" 564 | python-versions = "*" 565 | version = "0.2.2" 566 | 567 | [package.dependencies] 568 | pyasn1 = ">=0.4.1,<0.5.0" 569 | 570 | [[package]] 571 | category = "main" 572 | description = "Pygments is a syntax highlighting package written in Python." 573 | name = "pygments" 574 | optional = false 575 | platform = "any" 576 | python-versions = "*" 577 | version = "2.2.0" 578 | 579 | [[package]] 580 | category = "main" 581 | description = "" 582 | name = "pyhandsontable" 583 | optional = false 584 | platform = "*" 585 | python-versions = ">=3.5" 586 | version = "0.2.7.1" 587 | 588 | [package.dependencies] 589 | jinja2 = "^2.10" 590 | jupyter = "^1.0" 591 | notebook = "^5.6" 592 | 593 | [package.source] 594 | reference = "" 595 | type = "directory" 596 | url = "../pyhandsontable" 597 | 598 | [[package]] 599 | category = "main" 600 | description = "Extensions to the standard Python datetime module" 601 | name = "python-dateutil" 602 | optional = false 603 | platform = "*" 604 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 605 | version = "2.7.3" 606 | 607 | [package.dependencies] 608 | six = ">=1.5" 609 | 610 | [[package]] 611 | category = "main" 612 | description = "Python bindings for the winpty library" 613 | name = "pywinpty" 614 | optional = false 615 | platform = "*" 616 | python-versions = "*" 617 | version = "0.5.4" 618 | 619 | [[package]] 620 | category = "main" 621 | description = "Python bindings for 0MQ" 622 | name = "pyzmq" 623 | optional = false 624 | platform = "*" 625 | python-versions = ">=2.7,!=3.0*,!=3.1*,!=3.2*" 626 | version = "17.1.0" 627 | 628 | [[package]] 629 | category = "main" 630 | description = "Python HTTP for Humans." 631 | name = "requests" 632 | optional = false 633 | platform = "*" 634 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 635 | version = "2.19.1" 636 | 637 | [package.dependencies] 638 | certifi = ">=2017.4.17" 639 | chardet = ">=3.0.2,<3.1.0" 640 | idna = ">=2.5,<2.8" 641 | urllib3 = ">=1.21.1,<1.24" 642 | 643 | [[package]] 644 | category = "main" 645 | description = "Pure-Python RSA implementation" 646 | name = "rsa" 647 | optional = false 648 | platform = "UNKNOWN" 649 | python-versions = "*" 650 | version = "3.4.2" 651 | 652 | [package.dependencies] 653 | pyasn1 = ">=0.1.3" 654 | 655 | [[package]] 656 | category = "main" 657 | description = "scandir, a better directory iterator and faster os.walk()" 658 | name = "scandir" 659 | optional = false 660 | platform = "*" 661 | python-versions = "*" 662 | version = "1.7" 663 | 664 | [package.requirements] 665 | python = ">=2.6.0,<2.8.0 || >=3.2.0,<3.4.0" 666 | 667 | [[package]] 668 | category = "main" 669 | description = "Send file to trash natively under Mac OS X, Windows and Linux." 670 | name = "send2trash" 671 | optional = false 672 | platform = "*" 673 | python-versions = "*" 674 | version = "1.5.0" 675 | 676 | [[package]] 677 | category = "main" 678 | description = "Simple generic functions (similar to Python's own len(), pickle.dump(), etc.)" 679 | name = "simplegeneric" 680 | optional = false 681 | platform = "UNKNOWN" 682 | python-versions = "*" 683 | version = "0.8.1" 684 | 685 | [[package]] 686 | category = "main" 687 | description = "Python 2 and 3 compatibility utilities" 688 | name = "six" 689 | optional = false 690 | platform = "*" 691 | python-versions = "*" 692 | version = "1.11.0" 693 | 694 | [[package]] 695 | category = "main" 696 | description = "Terminals served to xterm.js using Tornado websockets" 697 | name = "terminado" 698 | optional = false 699 | platform = "*" 700 | python-versions = "*" 701 | version = "0.8.1" 702 | 703 | [package.dependencies] 704 | ptyprocess = "*" 705 | pywinpty = ">=0.5" 706 | tornado = ">=4" 707 | 708 | [[package]] 709 | category = "main" 710 | description = "Test utilities for code working with files and commands" 711 | name = "testpath" 712 | optional = false 713 | platform = "*" 714 | python-versions = "*" 715 | version = "0.3.1" 716 | 717 | [[package]] 718 | category = "main" 719 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 720 | name = "tornado" 721 | optional = false 722 | platform = "*" 723 | python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, != 3.3.*" 724 | version = "5.1" 725 | 726 | [[package]] 727 | category = "main" 728 | description = "Traitlets Python config system" 729 | name = "traitlets" 730 | optional = false 731 | platform = "Linux,Mac OS X,Windows" 732 | python-versions = "*" 733 | version = "4.3.2" 734 | 735 | [package.dependencies] 736 | decorator = "*" 737 | ipython-genutils = "*" 738 | six = "*" 739 | 740 | [package.dependencies.enum34] 741 | python = ">=3.3,<3.4" 742 | version = "*" 743 | 744 | [[package]] 745 | category = "main" 746 | description = "Type Hints for Python" 747 | name = "typing" 748 | optional = false 749 | platform = "*" 750 | python-versions = "*" 751 | version = "3.6.4" 752 | 753 | [package.requirements] 754 | python = "<=3.4" 755 | 756 | [[package]] 757 | category = "main" 758 | description = "URI templates" 759 | name = "uritemplate" 760 | optional = false 761 | platform = "*" 762 | python-versions = "*" 763 | version = "3.0.0" 764 | 765 | [[package]] 766 | category = "main" 767 | description = "HTTP library with thread-safe connection pooling, file post, and more." 768 | name = "urllib3" 769 | optional = false 770 | platform = "*" 771 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 772 | version = "1.23" 773 | 774 | [[package]] 775 | category = "main" 776 | description = "Measures number of Terminal column cells of wide-character codes" 777 | name = "wcwidth" 778 | optional = false 779 | platform = "UNKNOWN" 780 | python-versions = "*" 781 | version = "0.1.7" 782 | 783 | [[package]] 784 | category = "main" 785 | description = "Character encoding aliases for legacy web content" 786 | name = "webencodings" 787 | optional = false 788 | platform = "*" 789 | python-versions = "*" 790 | version = "0.5.1" 791 | 792 | [[package]] 793 | category = "main" 794 | description = "Enable Unicode input and display when running Python from Windows console." 795 | name = "win-unicode-console" 796 | optional = false 797 | platform = "UNKNOWN" 798 | python-versions = "*" 799 | version = "0.5" 800 | 801 | [package.requirements] 802 | platform = "win32" 803 | python = "<3.6" 804 | 805 | [metadata] 806 | content-hash = "9484ae61288e6d110ea5946d9dcb4d51b838e5b2a151f31d72eee8538ab406fb" 807 | platform = "*" 808 | python-versions = ">=3" 809 | 810 | [metadata.hashes] 811 | appnope = ["5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0", "8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"] 812 | backcall = ["38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", "bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"] 813 | beautifulsoup4 = ["11a9a27b7d3bddc6d86f59fb76afb70e921a25ac2d6cc55b40d072bd68435a76", "7015e76bf32f1f574636c4288399a6de66ce08fb7b2457f628a8d70c0fbabb11", "808b6ac932dccb0a4126558f7dfdcf41710dd44a4ef497a0bb59a77f9f078e89"] 814 | bleach = ["b8fa79e91f96c2c2cd9fd1f9eda906efb1b88b483048978ba62fef680e962b34", "eb7386f632349d10d9ce9d4a838b134d4731571851149f9cc2c05a9a837a9a44"] 815 | bs4 = ["36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"] 816 | cachetools = ["90f1d559512fc073483fe573ef5ceb39bf6ad3d39edc98dc55178a2b2b176fa3", "d1c398969c478d336f767ba02040fa22617333293fb0b8968e79b16028dfee35"] 817 | certifi = ["13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", "9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"] 818 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 819 | colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] 820 | decorator = ["2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", "c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c"] 821 | entrypoints = ["10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b", "d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"] 822 | enum34 = ["2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850", "644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a", "6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79", "8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1"] 823 | google-api-python-client = ["5d5cb02c6f3112c68eed51b74891a49c0e35263380672d662f8bfe85b8114d7c", "7cc47cf80b25ecd7f3d917ea247bb6c62587514e40604ae29c47c0e4ebd1174b"] 824 | google-auth = ["1745c9066f698eac3da99cef082914495fb71bc09597ba7626efbbb64c4acc57", "82a34e1a59ad35f01484d283d2a36b7a24c8c404a03a71b3afddd0a4d31e169f"] 825 | google-auth-httplib2 = ["098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445", "f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08"] 826 | html5lib = ["20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", "66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736"] 827 | httplib2 = ["e71daed9a0e6373642db61166fa70beecc9bf04383477f84671348c02a04cbdf"] 828 | idna = ["156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"] 829 | ipykernel = ["395f020610e33ffa0b0c9c0cd1a1d927d51ab9aa9f30a7ae36bb0c908a33e89c", "935941dba29d856eee34b8b5261d971bd5012547239ed73ddfff099143748c37", "c091449dd0fad7710ddd9c4a06e8b9e15277da306590bc07a3a1afa6b4453c8f"] 830 | ipython = ["a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", "eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c"] 831 | ipython-genutils = ["72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", "eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"] 832 | jedi = ["b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", "c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f"] 833 | jinja2 = ["74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"] 834 | jsonschema = ["000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08", "6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"] 835 | jupyter = ["3e1f86076bbb7c8c207829390305a2b1fe836d471ed54be66a3b8c41e7f46cc7", "5b290f93b98ffbc21c0c7e749f054b3267782166d72fa5e3ed1ed4eaf34a2b78", "d9dc4b3318f310e34c82951ea5d6683f67bed7def4b259fafbfe4f1beb1d8e5f"] 836 | jupyter-client = ["27befcf0446b01e29853014d6a902dd101ad7d7f94e2252b1adca17c3466b761", "59e6d791e22a8002ad0e80b78c6fd6deecab4f9e1b1aa1a22f4213de271b29ea"] 837 | jupyter-core = ["927d713ffa616ea11972534411544589976b2493fc7e09ad946e010aa7eb9970", "ba70754aa680300306c699790128f6fbd8c306ee5927976cbe48adacf240c0b7"] 838 | markdown = ["9ba587db9daee7ec761cfc656272be6aabe2ed300fece21208e4aab2e457bc8f", "a856869c7ff079ad84a3e19cd87a64998350c2b94e9e08e44270faef33400f81"] 839 | markupsafe = ["a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"] 840 | mistune = ["b4c512ce2fc99e5a62eb95a4aba4b73e5f90264115c40b70a21e1f7d4e0eac91", "bc10c33bfdcaa4e749b779f62f60d6e12f8215c46a292d05e486b869ae306619"] 841 | nbconvert = ["12b1a4671d4463ab73af6e4cbcc965b62254e05d182cd54995dda0d0ef9e2db9", "260d390b989a647575b8ecae2cd06a9eaead10d396733d6e50185d5ebd08996e"] 842 | nbformat = ["b9a0dbdbd45bb034f4f8893cafd6f652ea08c8c1674ba83f2dc55d3955743b0b", "f7494ef0df60766b7cabe0a3651556345a963b74dbc16bc7c18479041170d402"] 843 | notebook = ["66dd59e76e755584ae9450eb015c39f55d4bb1d8ec68f2c694d2b3cba7bf5c7e", "e2c8e931cc19db4f8c63e6a396efbc13a228b2cb5b2919df011b946f28239a08"] 844 | oauth2client = ["bd3062c06f8b10c6ef7a890b22c2740e5f87d61b6e1f4b1c90d069cdfc9dadb5", "cf061f52f75e91d489bf5c276498f8af2655fe331b454f10022441513cf445a6"] 845 | pandocfilters = ["b3dd70e169bb5449e6bc6ff96aea89c5eea8c5f6ab5e207fc2f521a2cf4a0da9"] 846 | parso = ["35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", "895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24"] 847 | pathlib2 = ["8eb170f8d0d61825e09a95b38be068299ddeda82f35e96c3301a8a5e7604cb83", "d1aa2a11ba7b8f7b21ab852b1fb5afb277e1bb99d5dfc663380b5015c0d80c5a"] 848 | pexpect = ["2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", "3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b"] 849 | pickleshare = ["84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", "c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5"] 850 | pillow = ["00def5b638994f888d1058e4d17c86dec8e1113c3741a0a8a659039aec59a83a", "026449b64e559226cdb8e6d8c931b5965d8fc90ec18ebbb0baa04c5b36503c72", "03dbb224ee196ef30ed2156d41b579143e1efeb422974719a5392fc035e4f574", "03eb0e04f929c102ae24bc436bf1c0c60a4e63b07ebd388e84d8b219df3e6acd", "087b0551ce2d19b3f092f2b5f071a065f7379e748867d070b29999cc83db15e3", "091a0656688d85fd6e10f49a73fa3ab9b37dbfcb2151f5a3ab17f8b879f467ee", "0f3e2d0a9966161b7dfd06d147f901d72c3a88ea1a833359b92193b8e1f68e1c", "114398d0e073b93e1d7da5b5ab92ff4b83c0180625c8031911425e51f4365d2e", "1be66b9a89e367e7d20d6cae419794997921fe105090fafd86ef39e20a3baab2", "1c5e93c40d4ce8cb133d3b105a869be6fa767e703f6eb1003eb4b90583e08a59", "1e977a3ed998a599bda5021fb2c2889060617627d3ae228297a529a082a3cd5c", "22cf3406d135cfcc13ec6228ade774c8461e125c940e80455f500638429be273", "24adccf1e834f82718c7fc8e3ec1093738da95144b8b1e44c99d5fc7d3e9c554", "2a3e362c97a5e6a259ee9cd66553292a1f8928a5bdfa3622fdb1501570834612", "3518f9fc666cbc58a5c1f48a6a23e9e6ceef69665eab43cdad5144de9383e72c", "3709339f4619e8c9b00f53079e40b964f43c5af61fb89a923fe24437167298bb", "3832e26ecbc9d8a500821e3a1d3765bda99d04ae29ffbb2efba49f5f788dc934", "452d159024faf37cc080537df308e8fa0026076eb38eb75185d96ed9642bd6d7", "4fd1f0c2dc02aaec729d91c92cd85a2df0289d88e9f68d1e8faba750bb9c4786", "4fda62030f2c515b6e2e673c57caa55cb04026a81968f3128aae10fc28e5cc27", "5044d75a68b49ce36a813c82d8201384207112d5d81643937fc758c05302f05b", "522184556921512ec484cb93bd84e0bab915d0ac5a372d49571c241a7f73db62", "5914cff11f3e920626da48e564be6818831713a3087586302444b9c70e8552d9", "653d48fe46378f40e3c2b892be88d8440efbb2c9df78559da44c63ad5ecb4142", "6661a7908d68c4a133e03dac8178287aa20a99f841ea90beeb98a233ae3fd710", "6735a7e560df6f0deb78246a6fe056cf2ae392ba2dc060ea8a6f2535aec924f1", "6d26a475a19cb294225738f5c974b3a24599438a67a30ed2d25638f012668026", "791f07fe13937e65285f9ef30664ddf0e10a0230bdb236751fa0ca67725740dd", "79258a8df3e309a54c7ef2ef4a59bb8e28f7e4a8992a3ad17c24b1889ced44f3", "7d74c20b8f1c3e99d3f781d3b8ff5abfefdd7363d61e23bdeba9992ff32cc4b4", "81918afeafc16ba5d9d0d4e9445905f21aac969a4ebb6f2bff4b9886da100f4b", "8194d913ca1f459377c8a4ed8f9b7ad750068b8e0e3f3f9c6963fcc87a84515f", "84d5d31200b11b3c76fab853b89ac898bf2d05c8b3da07c1fcc23feb06359d6e", "989981db57abffb52026b114c9a1f114c7142860a6d30a352d28f8cbf186500b", "a3d7511d3fad1618a82299aab71a5fceee5c015653a77ffea75ced9ef917e71a", "a4a6ac01b8c2f9d2d83719f193e6dea493e18445ce5bfd743d739174daa974d9", "acb90eb6c7ed6526551a78211d84c81e33082a35642ff5fe57489abc14e6bf6e", "b3ef168d4d6fd4fa6685aef7c91400f59f7ab1c0da734541f7031699741fb23f", "c1c5792b6e74bbf2af0f8e892272c2a6c48efa895903211f11b8342e03129fea", "c5dcb5a56aebb8a8f2585042b2f5c496d7624f0bcfe248f0cc33ceb2fd8d39e7", "d16f90810106822833a19bdb24c7cb766959acf791ca0edf5edfec674d55c8ee", "dcdc9cd9880027688007ff8f7c8e7ae6f24e81fae33bfd18d1e691e7bda4855f", "e2807aad4565d8de15391a9548f97818a14ef32624015c7bf3095171e314445e", "e2bed4a04e2ca1050bb5f00865cf2f83c0b92fd62454d9244f690fcd842e27a4", "e87a527c06319428007e8c30511e1f0ce035cb7f14bb4793b003ed532c3b9333", "ebcfc33a6c34984086451e230253bc33727bd17b4cdc4b39ec03032c3a6fc9e9", "f63e420180cbe22ff6e32558b612e75f50616fc111c5e095a4631946c782e109", "f7717eb360d40e7598c30cc44b33d98f79c468d9279379b66c1e28c568e0bf47", "f8582e1ab155302ea9ef1235441a0214919f4f79c4c7c21833ce9eec58181781", "f8b3d413c5a8f84b12cd4c5df1d8e211777c9852c6be3ee9c094b626644d3eab"] 851 | prometheus-client = ["69494dc1ac967c0f626c8193e439755c2b95dd4ed22ef31c277601778a50c7ff"] 852 | prompt-toolkit = ["1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", "3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", "858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917"] 853 | ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] 854 | pyasn1 = ["24f21b4fd2dc2b344dee2205fa3930464aa21292216d3d6e39007a2e059e21af", "2f57960dc7a2820ea5a1782b872d974b639aa3b448ac6628d1ecc5d0fe3986f2", "3651774ca1c9726307560792877db747ba5e8a844ea1a41feb7670b319800ab3", "602fda674355b4701acd7741b2be5ac188056594bf1eecf690816d944e52905e", "8fb265066eac1d3bb5015c6988981b009ccefd294008ff7973ed5f64335b0f2d", "9334cb427609d2b1e195bb1e251f99636f817d7e3e1dffa150cb3365188fb992", "9a15cc13ff6bf5ed29ac936ca941400be050dff19630d6cd1df3fb978ef4c5ad", "a66dcda18dbf6e4663bde70eb30af3fc4fe1acb2d14c4867a861681887a5f9a2", "ba77f1e8d7d58abc42bfeddd217b545fdab4c1eeb50fd37c2219810ad56303bf", "cdc8eb2eaafb56de66786afa6809cd9db2df1b3b595dcb25aa5b9dc61189d40a", "d01fbba900c80b42af5c3fe1a999acf61e27bf0e452e0f1ef4619065e57622da", "f281bf11fe204f05859225ec2e9da7a7c140b65deccd8a4eb0bc75d0bd6949e0", "fb81622d8f3509f0026b0683fe90fea27be7284d3826a5f2edf97f69151ab0fc"] 855 | pyasn1-modules = ["077250b34432520430bc1c80dcbda4e354090785567c33ded35faa6df8d24753", "0da2f947e8ad2697e86fe5fd0e55a4093a2fd79d839c9e19c34e28097db7002c", "35ff894a0b5df8e28b700126b2869c7dcfb2b2db5bc82e5d5e82547069241553", "44688b94841349648b1e1a5a7a3d96e6596d5d4f21d0b59a82307e153c4dc74b", "833716dde880a7f2f2ccdeea9a096842626981ff2a477d8b318c0906367ac11b", "a0cf3e1842e7c60fde97cb22d275eb6f9524f5c5250489e292529de841417547", "a38a8811ea784c0136abfdba73963876328f66172db21a05a82f9515909bfb4e", "a728bb9502d1fdc104c66f24a176b6a70a32e89d1d8a5b55c959233ed51c67be", "c30a098435ea0989c37005a971843e9d3966c7f6d056ddbf052e5061c06e3291", "c355a45b32c5bc1d9893eceb704b0cfcd1126f91b5a7b9ee64c1c05383283381", "e64679de1940f41ead5170fce364d54e7b9e2e862f064727b6bcb5cee753b7a2", "ed71d20225c356881c29f0b1d7a0d6521563a389d9478e8f95d798cc5ba07b88", "f183f0940b9f5ed2ad9d04c80cab2451440fa9af4fc959d85113fadd2e777962"] 856 | pygments = ["78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"] 857 | pyhandsontable = [] 858 | python-dateutil = ["1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", "e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"] 859 | pywinpty = ["349eef36414b038426e65d96ecccfa581c437562cc164fb4faffe6f46963bc80", "4617637c38ae9099a99f73d8dbeb9c752743693bd1dca6ea3b1d520a7248ebf3", "4ee8193b19d77ab59097a000a2c52b36e768e92263812e0c0b40306be8927fb4", "4f6c850db79dd19b1d842d81a8c08fd7efad5e160a1effbba10ba738a5a35cb2", "4fd720b20bb69f1b7ca2060e84503ae843972fcb006ae6e8ddd6ab212fe8911c", "79f2b4584111e36826e587d33eb4e7416a12ae1d6c094cb554e873c5c162fa5f", "87ae1a2301fbce7a3005dac7cdf8ce8a4162f05130348234b87caef260771e96"] 860 | pyzmq = ["096b72e48dc4fb6afac4dc862626b103287176be7c0f49b52782689eac4bf376", "1c0d469837c54df4b3b8424c79f102a2a4ccc7e1e7f8816fcdcf39c1e6406b19", "2199f753a230e26aec5238b0518b036780708a4c887d4944519681a920b9dee4", "22402d2a55dcc5973b1356be12dbc4417350fcc85a6da3af2984271e769a28f4", "4166c2e45b5d43bc1da49a7f8518c2ced11227fb69b35405940442b92b0b0c34", "488ab43a9c4a710264987e7efe5d53384858281bd6540db0a47e7bb6c3bf7ec5", "8d757eaf5eb3b27ab449343e55b8e8f442a3de67ccdfca893cefe8964d44a147", "8e576262221c7fbee894cf105eb975f1385fc91988ce6120cdb2305195e7a9f6", "90b38559b33f544ebaa35fcb8d4091cb35c689a3cfe92e584e36c746f648c12b", "911f457a0b5c0e0c857a71bd2abbb96d15c695a11cf61caa43059143ed01bb6e", "941c9676bc2f3dbab528a4cd42a9dd80310156ed6014276f16288f25c9ff4da2", "965d50febc5402e7fb93c432155463b4184a79bec040cdeb3b006b278b3a544f", "98fe34bbbe5a516e75ac16f1ea585d9c1dda2067b6c9f4da174add6c952883b6", "9dc60c4a8299aa77082493231045d15b58c7875cb9f3dbee6e11a86356bd9e24", "a3d1c56387332063a5492c186fb504a7b6dfbdf6f0ce0a7388fed722998e3b0f", "a9660113ff4c52160f734704af8d351326cc6eabf1dbd135cf02c0ded02e1d9d", "b097f32a1ee00e0b2aa9776e0112a73655484d2e7c0634757c84c036c8be0706", "bcd81ab868e916fc22172667e528211b21e2dcab9af2e9fb3ee631d3eb946893", "bd1c9cbb6d1032a61a8ab0e77b1c83caa7b04e77a6736ff23d213de6bde92622", "cfe8d245bd62b3f87e120029f5a83f47055d3bb2d2b7f91566dd43487dedd6e3", "d74052450985befe7d983abb1daab6112b5cb78f1258731afe04c55dbb504739", "df65629511d6037045b2b4d87b17b0a984d7ae6af44e0d87c350c93e6685d746", "e724a5117044490cf0f55b5025bba4b909de0be4f47f6a48ad7c24d6a8e7e48c", "fafcb5be16c0012ada037b8eaf63d5b25d421680072eeb6df4a2d90599d962e6", "fdced6cf08aaa5b0dacfa4562e518ffb562bb09a90483f920cf6c4ecabd3f379"] 861 | requests = ["63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"] 862 | rsa = ["25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5", "43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd"] 863 | scandir = ["24f32112c483ac6c4a40b62f1282e61ecca7977153b66a0d26a9583a716dcb64", "6c80092f8fe3e62c3da3110067589c6661c722b0889906d2974e5150f1314523", "7729b8444c5f5187649ff58501e7c2ad22b84d7dc28f738f64c5b615913fec22", "8e3ca5925cc13787aeafbf08f055a8066c091fc20bfa8783235b916cf047afbe", "96dfc553f50946deb6d1cd762bac5cf122832c4aa253c885ca357ef53dd8d072", "b2d55be869c4f716084a19b1e16932f0769711316ba62de941320bf2be84763d", "b55a091b91f9e6c9c7129889b2f58df329530172a99172de9e784545342a45e6", "b6cb611a18a828146a178362a36a2c6557c51c596ded4314cb516dd8c947b4ce", "d985e36eb3effebb20434e6cd7495440b4ba676a22f3ec61e9fff9f3e2995238", "f39dd5affde2860fb28176d2233f318ccca97c55019407ee8172b3fba0b211db", "f91418e82edb5a43b020fa15e30a41d730b71c5957536749366bf63cc05427b1"] 864 | send2trash = ["60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2", "f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"] 865 | simplegeneric = ["dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173"] 866 | six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"] 867 | terminado = ["55abf9ade563b8f9be1f34e4233c7b7bde726059947a593322e8a553cc4c067a", "65011551baff97f5414c67018e908110693143cfbaeb16831b743fe7cad8b927"] 868 | testpath = ["039fa6a6c9fd3488f8336d23aebbfead5fa602c4a47d49d83845f55a595ec1b4", "0d5337839c788da5900df70f8e01015aec141aa3fe7936cb0d0a2953f7ac7609"] 869 | tornado = ["1c0816fc32b7d31b98781bd8ebc7a9726d7dce67407dc353a2e66e697e138448", "4f66a2172cb947387193ca4c2c3e19131f1c70fa8be470ddbbd9317fd0801582", "5327ba1a6c694e0149e7d9126426b3704b1d9d520852a3e4aa9fc8fe989e4046", "6a7e8657618268bb007646b9eae7661d0b57f13efc94faa33cd2588eae5912c9", "a9b14804783a1d77c0bd6c66f7a9b1196cbddfbdf8bceb64683c5ae60bd1ec6f", "c58757e37c4a3172949c99099d4d5106e4d7b63aa0617f9bb24bfbff712c7866", "d8984742ce86c0855cccecd5c6f54a9f7532c983947cff06f3a0e2115b47f85c"] 870 | traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] 871 | typing = ["3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf", "b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8", "d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"] 872 | uritemplate = ["01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd", "1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd", "c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d"] 873 | urllib3 = ["a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"] 874 | wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] 875 | webencodings = ["a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", "b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"] 876 | win-unicode-console = ["d4142d4d56d46f449d6f00536a73625a871cba040f0bc1a2e305a04578f07d1e"] 877 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "gflashcards" 3 | version = "0.2.0" 4 | description = "Google Sheets-based flashcard maker" 5 | authors = ["Pacharapol Withayasakpunt "] 6 | license = "Apache-2.0" 7 | 8 | [tool.poetry.dependencies] 9 | python = ">=3" 10 | jupyter = "^1.0" 11 | notebook = "^5.6" 12 | google-api-python-client = "^1.7" 13 | oauth2client = "^4.1" 14 | markdown = "^2.6" 15 | pillow = "^5.2" 16 | requests = "^2.19" 17 | #pyhandsontable = "^0.2.7.1" 18 | bs4 = "^0.0.1" 19 | pyhandsontable = {path = "../pyhandsontable"} 20 | 21 | [tool.poetry.dev-dependencies] 22 | -------------------------------------------------------------------------------- /user/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | --------------------------------------------------------------------------------