├── requirements.txt ├── awesome ├── __init__.py ├── parsers │ ├── go.py │ ├── ruby.py │ ├── php.py │ ├── cpp.py │ ├── python.py │ ├── vue.py │ ├── nodejs.py │ ├── swift.py │ ├── scala.py │ ├── awesome.py │ ├── elixir.py │ ├── java.py │ ├── erlang.py │ ├── javascript.py │ ├── rust.py │ ├── shell.py │ ├── android.py │ ├── ios.py │ └── __init__.py ├── parser_loader.py ├── __main__.py ├── cache.py └── tui.py ├── CONTRIBUTORS.txt ├── Pipfile ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── setup.py ├── CONTRIBUTING.md ├── README.md └── Pipfile.lock /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.18.4 2 | -------------------------------------------------------------------------------- /awesome/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | awesome-finder - A TUI based awesome curated list finder 3 | """ 4 | __version__ = '1.2.3' 5 | __author__ = 'mingrammer' 6 | __license__ = 'MIT' 7 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | mingrammer 2 | AhnSeongHyun 3 | MatToufoutu 4 | zrong 5 | jneidel 6 | r567tw 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | 9 | [dev-packages] 10 | twine = "*" 11 | 12 | [requires] 13 | python_version = "3.6" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source 2 | __pycache__/ 3 | *.pyc 4 | 5 | # IDE and Editor 6 | .idea/ 7 | .vscode/ 8 | 9 | # Build and Packages 10 | awesome_finder.egg-info 11 | dist/ 12 | build/ 13 | 14 | # Trash 15 | .DS_Store 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 mingrammer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /awesome/parsers/go.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeGoParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'go' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/avelino/awesome-go/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Tutorials](#tutorials)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | return awesome_blocks 35 | -------------------------------------------------------------------------------- /awesome/parsers/ruby.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeRubyParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'ruby' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/markets/awesome-ruby/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('* [Resources](#resources)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('#'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | return awesome_blocks 35 | -------------------------------------------------------------------------------- /awesome/parsers/php.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomePHPParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'php' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/ziadoz/awesome-php/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [PHP Internals Reading](#php-internals-reading)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | return awesome_blocks 35 | -------------------------------------------------------------------------------- /awesome/parsers/cpp.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeCppParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'cpp' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/fffaraz/awesome-cpp/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##') and not line.startswith('#### *If'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | return awesome_blocks 35 | -------------------------------------------------------------------------------- /awesome/parsers/python.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomePythonParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'python' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/vinta/awesome-python/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('---')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('#'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/vue.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeVueParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'vue' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/vuejs/awesome-vue/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split(' - [Prerendering](#prerendering)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('###'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('
'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/nodejs.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeNodejsParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'nodejs' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/sindresorhus/awesome-nodejs/master/readme.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('\n\n## Packages')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('###'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('## License'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/swift.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeSwiftParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'swift' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/matteocrippa/awesome-swift/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Video](#video)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('#'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/scala.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeScalaParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'scala' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/lauris/awesome-scala/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('#'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/awesome.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'awesome' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/sindresorhus/awesome/main/readme.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('https://awesome.re')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Ignore last useless parts 20 | if line.startswith('## License'): 21 | break 22 | # Parse the header title 23 | elif line.startswith('##'): 24 | plain_title = self.parse_category_title(line) 25 | awesome_blocks.append({ 26 | 'type': 'category', 27 | 'line': plain_title, 28 | }) 29 | # Parse the list item 30 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 31 | plain_line, link = self.parse_link_line(line) 32 | awesome_blocks.append({ 33 | 'type': 'awesome', 34 | 'line': plain_line, 35 | 'link': link, 36 | }) 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/elixir.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeElixirParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'elixir' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/h4cc/awesome-elixir/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('#'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/java.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeJavaParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'java' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/akullpp/awesome-java/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split(' - [Websites](#websites)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('## Contributing'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/erlang.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeErlangParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'erlang' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/drobakowski/awesome-erlang/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('#'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/javascript.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeJavaScriptParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'javascript' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/sorrycc/awesome-javascript/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('----')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('# Contributing'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/rust.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeRustParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'rust' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/rust-unofficial/awesome-rust/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split(' - [License](#license)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Ignore last useless parts 20 | if line.startswith('## License'): 21 | break 22 | # Parse the header title 23 | elif line.startswith('##'): 24 | plain_title = self.parse_category_title(line) 25 | awesome_blocks.append({ 26 | 'type': 'category', 27 | 'line': plain_title, 28 | }) 29 | # Parse the list item 30 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 31 | plain_line, link = self.parse_link_line(line) 32 | awesome_blocks.append({ 33 | 'type': 'awesome', 34 | 'line': plain_line, 35 | 'link': link, 36 | }) 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/shell.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeShellParser(AbstractAwesomeParser): 5 | AWSOME_TITLE = 'shell' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/alebcay/awesome-shell/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Other Awesome Lists](#other-awesome-lists)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith(' -['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('# Other'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/android.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeAndroidParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'android' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/JStumpp/awesome-android/master/readme.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('## Contributing'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parsers/ios.py: -------------------------------------------------------------------------------- 1 | from . import AbstractAwesomeParser 2 | 3 | 4 | class AwesomeIosParser(AbstractAwesomeParser): 5 | AWESOME_TITLE = 'ios' 6 | AWESOME_README_URL = 'https://raw.githubusercontent.com/vsouza/awesome-ios/master/README.md' 7 | 8 | def find_content(self): 9 | readme = self.read_readme() 10 | content = readme.split('- [Contributing](#contributing-and-license)')[1] 11 | lines = [] 12 | for line in content.split('\n'): 13 | lines.append(line) 14 | return lines 15 | 16 | def parse_awesome_content(self, content): 17 | awesome_blocks = [] 18 | for line in content: 19 | # Parse the header title 20 | if line.startswith('##'): 21 | plain_title = self.parse_category_title(line) 22 | awesome_blocks.append({ 23 | 'type': 'category', 24 | 'line': plain_title, 25 | }) 26 | # Parse the list item 27 | elif line.strip().startswith('* [') or line.strip().startswith('- ['): 28 | plain_line, link = self.parse_link_line(line) 29 | awesome_blocks.append({ 30 | 'type': 'awesome', 31 | 'line': plain_line, 32 | 'link': link, 33 | }) 34 | # Ignore last useless parts 35 | elif line.startswith('## Contributing and License'): 36 | break 37 | return awesome_blocks 38 | -------------------------------------------------------------------------------- /awesome/parser_loader.py: -------------------------------------------------------------------------------- 1 | from .parsers.android import AwesomeAndroidParser 2 | from .parsers.awesome import AwesomeParser 3 | from .parsers.elixir import AwesomeElixirParser 4 | from .parsers.erlang import AwesomeErlangParser 5 | from .parsers.go import AwesomeGoParser 6 | from .parsers.ios import AwesomeIosParser 7 | from .parsers.java import AwesomeJavaParser 8 | from .parsers.javascript import AwesomeJavaScriptParser 9 | from .parsers.php import AwesomePHPParser 10 | from .parsers.python import AwesomePythonParser 11 | from .parsers.ruby import AwesomeRubyParser 12 | from .parsers.rust import AwesomeRustParser 13 | from .parsers.scala import AwesomeScalaParser 14 | from .parsers.shell import AwesomeShellParser 15 | from .parsers.swift import AwesomeSwiftParser 16 | from .parsers.vue import AwesomeVueParser 17 | from .parsers.nodejs import AwesomeNodejsParser 18 | from .parsers.cpp import AwesomeCppParser 19 | 20 | 21 | def load_parsers(): 22 | return { 23 | 'android': AwesomeAndroidParser, 24 | 'awesome': AwesomeParser, 25 | 'elixir': AwesomeElixirParser, 26 | 'erlang': AwesomeErlangParser, 27 | 'go': AwesomeGoParser, 28 | 'ios': AwesomeIosParser, 29 | 'java': AwesomeJavaParser, 30 | 'javascript': AwesomeJavaScriptParser, 31 | 'php': AwesomePHPParser, 32 | 'python': AwesomePythonParser, 33 | 'ruby': AwesomeRubyParser, 34 | 'rust': AwesomeRustParser, 35 | 'scala': AwesomeScalaParser, 36 | 'shell': AwesomeShellParser, 37 | 'swift': AwesomeSwiftParser, 38 | 'vue': AwesomeVueParser, 39 | 'nodejs': AwesomeNodejsParser, 40 | 'cpp': AwesomeCppParser, 41 | } 42 | -------------------------------------------------------------------------------- /awesome/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import os 5 | 6 | from awesome import __author__, __version__ 7 | from awesome.parser_loader import load_parsers 8 | from awesome.tui import SearchScreen 9 | 10 | awesome_parsers = load_parsers() 11 | 12 | 13 | def set_short_esc_delay(): 14 | """Make the ESC key delay short""" 15 | os.environ.setdefault('ESCDELAY', '50') 16 | 17 | 18 | def get_awesome_blocks(awesome_title, force): 19 | parser = awesome_parsers[awesome_title](force_request=force) 20 | awesome_blocks = parser.parse() 21 | return awesome_blocks 22 | 23 | 24 | def parse_command(): 25 | parser = argparse.ArgumentParser(description='awesome command') 26 | parser.add_argument('--version', action='version', version='awesome-finder version {version}, (c) 2017-2019 by {author}.'.format(version=__version__, author=__author__)) 27 | 28 | subparsers = parser.add_subparsers(dest='title', title='title', description='the title of awesome you want to find') 29 | subparsers.required = True 30 | 31 | # Register awesome commands 32 | for title in awesome_parsers.keys(): 33 | if title == 'awesome': 34 | subparser = subparsers.add_parser(title, help='search the {}'.format(title)) 35 | else: 36 | subparser = subparsers.add_parser(title, help='search the awesome-{}'.format(title)) 37 | subparser.add_argument('--force', '-f', type=bool, nargs='?', const=True, default=False) 38 | subparser.add_argument('--query', '-q', type=str, default='', help='pass the initial query') 39 | return parser.parse_args() 40 | 41 | 42 | def main(): 43 | set_short_esc_delay() 44 | 45 | args = parse_command() 46 | awesome_blocks = get_awesome_blocks(args.title, args.force) 47 | initial_query = args.query 48 | 49 | screen = SearchScreen(args.title, awesome_blocks, initial_query=initial_query) 50 | screen.run() 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /awesome/cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | CACHE_DIRECTORY = os.path.join(os.path.expanduser('~'), '.awesome_cache') 4 | 5 | 6 | def exists(awesome_title): 7 | """Check the awesome repository is cached 8 | 9 | Args: 10 | awesome_title: Awesome repository title 11 | 12 | Returns: 13 | True if exists, False otherwise 14 | """ 15 | awesome_cache_directory = os.path.join(CACHE_DIRECTORY, awesome_title) 16 | awesome_cached_readme = os.path.join(awesome_cache_directory, 'README.md') 17 | return os.path.exists(awesome_cached_readme) 18 | 19 | 20 | def save_readme(awesome_title, readme): 21 | """Save the README on local cache directory 22 | 23 | Args: 24 | awesome_title: Awesome repository title 25 | readme: Readme content 26 | 27 | Returns: 28 | None 29 | """ 30 | awesome_cache_directory = os.path.join(CACHE_DIRECTORY, awesome_title) 31 | awesome_cached_readme = os.path.join(awesome_cache_directory, 'README.md') 32 | if not os.path.exists(CACHE_DIRECTORY): 33 | os.mkdir(CACHE_DIRECTORY) 34 | if not os.path.exists(awesome_cache_directory): 35 | os.mkdir(awesome_cache_directory) 36 | with open(awesome_cached_readme, 'w+', encoding='utf8') as readme_md: 37 | readme_md.write(readme) 38 | 39 | 40 | def load_readme(awesome_title): 41 | """Loads the cached readme content 42 | 43 | Args: 44 | awesome_title: Awesome repository title 45 | 46 | Returns: 47 | A string of cached readme content, None if there is no cached readme 48 | """ 49 | awesome_cache_directory = os.path.join(CACHE_DIRECTORY, awesome_title) 50 | awesome_cached_readme = os.path.join(awesome_cache_directory, 'README.md') 51 | if not os.path.exists(awesome_cache_directory): 52 | raise NotADirectoryError('{} does not exists'.format(awesome_cache_directory)) 53 | if not os.path.exists(awesome_cached_readme): 54 | raise FileNotFoundError('{} does not exists'.format(awesome_cached_readme)) 55 | with open(awesome_cached_readme, 'r', encoding='utf8') as readme_md: 56 | return readme_md.read() 57 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.2.3](https://github.com/mingrammer/awesome-finder/releases/tag/v1.2.3) - 2019-12-07 6 | 7 | ### Fixed 8 | 9 | - Update all dependencies 10 | 11 | ## [1.2.2](https://github.com/mingrammer/awesome-finder/releases/tag/v1.2.2) - 2019-12-07 12 | 13 | ### Fixed 14 | 15 | - Fix split text for awesome-python, awesome-php , awesome-ios 16 | 17 | ## [1.2.1](https://github.com/mingrammer/awesome-finder/releases/tag/v1.2.1) - 2018-04-24 18 | 19 | ### Fixed 20 | 21 | - Use portable shebang line for python3 22 | 23 | ## [1.2.0](https://github.com/mingrammer/awesome-finder/releases/tag/v1.2.0) - 2018-03-04 24 | 25 | ### Added 26 | 27 | - Support paging with KEY_LEFT/KEY_RIGHT 28 | - Support awesome-erlang 29 | 30 | ## [1.1.3](https://github.com/mingrammer/awesome-finder/releases/tag/v1.1.3) - 2018-02-06 31 | 32 | ### Added 33 | 34 | - Support initial query (-q) 35 | 36 | ## [1.1.2](https://github.com/mingrammer/awesome-finder/releases/tag/v1.1.2) - 2018-01-21 37 | 38 | ### Added 39 | 40 | - Add parsers for awesome-nodejs/vue 41 | 42 | ## [1.1.1](https://github.com/mingrammer/awesome-finder/releases/tag/v1.1.1) - 2017-10-12 43 | 44 | ### Added 45 | 46 | - Add `--version` option 47 | 48 | ## [1.1.0](https://github.com/mingrammer/awesome-finder/releases/tag/v1.1.0) - 2017-10-12 49 | 50 | ### Added 51 | 52 | * Create CHANGELOG.md 53 | * Add `requirements.txt` 54 | 55 | ### Changed 56 | 57 | * Rename the `awesome` binary to `awesome-hub` to avoid the conflict with awesome binary of `awesome-wm` 58 | 59 | ## [1.0.6](https://github.com/mingrammer/awesome-finder/releases/tag/v1.0.6) - 2017-09-29 60 | 61 | ### Changed 62 | 63 | * Improve the parsing regex for that make it possible to recognize the emoji text 64 | 65 | ### Fixed 66 | 67 | * Fix parsing problems for `awesome-java` and `awesome-php` 68 | 69 | ## [1.0.5](https://github.com/mingrammer/awesome-finder/releases/tag/v1.0.5) - 2017-09-13 70 | 71 | ### Added 72 | 73 | * Add parsers for awesome-android/ios 74 | 75 | ### Fixed 76 | 77 | * Fix a bug for trying to open the non exist link of empty matched blocks 78 | 79 | ## [1.0.4](https://github.com/mingrammer/awesome-finder/releases/tag/v1.0.4) - 2017-09-11 80 | 81 | ### Fixed 82 | 83 | * Fix a bug that cache directory was not created 84 | 85 | 86 | ## [1.0.0](https://github.com/mingrammer/awesome-finder/releases/tag/v1.0.0) - 2017-09-11 87 | 88 | ### Added 89 | 90 | * Parses the awesome repository 91 | * Supports some awesome repository 92 | * Open a link on awesome finder 93 | * Supports multiple keywords query 94 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import sys 4 | from shutil import rmtree 5 | 6 | from setuptools import Command, find_packages, setup 7 | 8 | import awesome 9 | 10 | HERE = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | # Package meta-data. 13 | NAME = 'awesome-finder' 14 | DESCRIPTION = 'A TUI based awesome curated list finder' 15 | URL = 'https://github.com/mingrammer/awesome-finder' 16 | EMAIL = 'mingrammer@gmail.com' 17 | 18 | # What packages are required for this module to be executed? 19 | REQUIRED = [] 20 | with io.open(os.path.join(HERE, 'requirements.txt'), encoding='utf-8') as f: 21 | for line in f: 22 | line = line.strip() 23 | if not line: 24 | continue 25 | REQUIRED.append(line) 26 | 27 | # Import the README and use it as the long-description. 28 | # Note: this will only work if 'README.rst' is present in your MANIFEST.in file! 29 | with io.open(os.path.join(HERE, 'README.md'), encoding='utf-8') as f: 30 | long_description = '\n' + f.read() 31 | 32 | 33 | class PublishCommand(Command): 34 | """Support setup.py publish.""" 35 | 36 | description = 'Build and publish the package.' 37 | user_options = [] 38 | 39 | @staticmethod 40 | def status(s): 41 | """Prints things in bold.""" 42 | print('\033[1m{0}\033[0m'.format(s)) 43 | 44 | def initialize_options(self): 45 | pass 46 | 47 | def finalize_options(self): 48 | pass 49 | 50 | def run(self): 51 | try: 52 | self.status('Removing previous builds…') 53 | rmtree(os.path.join(HERE, 'dist')) 54 | except OSError: 55 | pass 56 | 57 | self.status('Building Source and Wheel (universal) distribution…') 58 | os.system( 59 | '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 60 | 61 | self.status('Uploading the package to PyPi via Twine…') 62 | os.system('twine upload dist/*') 63 | 64 | sys.exit() 65 | 66 | 67 | setup( 68 | name=NAME, 69 | version=awesome.__version__, 70 | description=DESCRIPTION, 71 | long_description=long_description, 72 | long_description_content_type='text/markdown', 73 | author=awesome.__author__, 74 | author_email=EMAIL, 75 | url=URL, 76 | keywords='awesome finder curses tui', 77 | packages=find_packages(), 78 | entry_points={ 79 | 'console_scripts': ['awesome-hub=awesome.__main__:main'], 80 | }, 81 | install_requires=REQUIRED, 82 | include_package_data=True, 83 | license=awesome.__license__, 84 | python_requires='>=3', 85 | classifiers=[ 86 | 'License :: OSI Approved :: MIT License', 87 | 'Programming Language :: Python', 88 | 'Programming Language :: Python :: 3', 89 | 'Programming Language :: Python :: 3.3', 90 | 'Programming Language :: Python :: 3.4', 91 | 'Programming Language :: Python :: 3.5', 92 | 'Programming Language :: Python :: 3.6', 93 | 'Programming Language :: Python :: 3.7', 94 | 'Programming Language :: Python :: 3.8', 95 | 'Programming Language :: Python :: Implementation :: CPython', 96 | 'Programming Language :: Python :: Implementation :: PyPy', 97 | 'Topic :: Terminals', 98 | 99 | ], 100 | # $ setup.py publish support. 101 | cmdclass={ 102 | 'publish': PublishCommand, 103 | }, 104 | ) 105 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you want to add other awesome repositories, you must implement the README parsers for them on `awesome/parsers` named `.py` (example: `awesome/parsers/ruby.py`) and register the parser to `awesome/parser_loader.py` 4 | 5 | <br> 6 | 7 | ## How it works? 8 | 9 | 1. Make a parser for a specific awesome repository to `awesome/parsers/` 10 | 2. Register this parser to `awesome/parser_loader.py` 11 | 3. The main will load the parsers automatically 12 | 13 | <br> 14 | 15 | ## How to make parsers 16 | 17 | All parsers have same form based on `AbstractAwesomeParser` on `awesome/parsers/__init__.py`. You need to implement the `find_content` method which finds only awesome content and `parse_awesome_content` which parses the content line by line. (**category header** or **line with link**) 18 | 19 | The awesome READMEs have common format as following: 20 | 21 | * TOC will be not used 22 | * Separator will be used for getting only awesome content without TOC 23 | * **category header** means `## category` 24 | * **line with link** means `* [title](link) - description` 25 | * `# others` is useless part (Optional. Some repositories does not have it) 26 | 27 | ``` 28 | ------------------------------------ 29 | ... table of content ... <- TOC 30 | 31 | {separator between TOC and content} <- Separator such as '----' 32 | 33 | ## category <- Awesome content 34 | * [title](link) - description <- 35 | * [title](link) - description <- 36 | * [title](link) - description <- 37 | 38 | # others <- Useless parts 39 | * ... <- 40 | ------------------------------------ 41 | ``` 42 | 43 | This is an example [`AwesomePythonParser`](/awesome/parsres/python.py) for **awesome-python** repository 44 | 45 | ```Python 46 | from . import AbstractAwesomeParser 47 | 48 | 49 | class AwesomePythonParser(AbstractAwesomeParser): 50 | AWESOME_TITLE = 'python' 51 | AWESOME_README_URL = 'https://raw.githubusercontent.com/vinta/awesome-python/master/README.md' 52 | 53 | def find_content(self): 54 | readme = self.read_readme() 55 | content = readme.split('- - -')[1] # '- - -' is separator 56 | lines = [] 57 | for line in content.split('\n'): 58 | lines.append(line) 59 | return lines 60 | 61 | def parse_awesome_contents(self, content): 62 | awesome_blocks = [] 63 | for line in content: 64 | # Parse the header title 65 | if line.startswith('##'): # 'category header' 66 | plain_title = self.parse_category_title(line) 67 | awesome_blocks.append({ 68 | 'type': 'category', 69 | 'line': plain_title, 70 | }) 71 | # Parse the list item 72 | elif line.strip().startswith('* ['): # line with link 73 | plain_line, link = self.parse_link_line(line) 74 | awesome_blocks.append({ 75 | 'type': 'awesome', 76 | 'line': plain_line, 77 | 'link': link, 78 | }) 79 | # Ignore last useless parts 80 | elif line.startswith('#'): # useless part of awesome-python 81 | break 82 | return awesome_blocks 83 | ``` 84 | 85 | <br> 86 | 87 | ## Development 88 | 89 | You can test it on local with `pip install -e .` or `pip3 install -e .`. 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <br><br> 2 | 3 | <h1 align="center">Awesome Finder</h1> 4 | 5 | <p align="center"> 6 | <a href="/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg"/></a> 7 | <a href="https://app.fossa.io/projects/git%2Bgithub.com%2Fmingrammer%2Fawesome-finder?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmingrammer%2Fawesome-finder.svg?type=shield"/></a> 8 | <a href="https://badge.fury.io/py/awesome-finder"><img src="https://badge.fury.io/py/awesome-finder.svg"/></a> 9 | <a href="https://docs.python.org/3/index.html"><img src="https://img.shields.io/badge/python-3.5,3.6-blue.svg"/></a> 10 | <a href="https://www.python.org/dev/peps/pep-0008"><img src="https://img.shields.io/badge/code%20style-PEP8-brightgreen.svg"/></a> 11 | </p> 12 | 13 | <p align="center"> 14 | Search the awesome curated list without browser 15 | </p> 16 | 17 | <br><br><br> 18 | 19 | > What does mean awesome? The awesome series provide a curated list of awesome frameworks, libraries, software and resources for a specific topic. An example is [awesome-python](https://github.com/vinta/awesome-python) 20 | 21 | A TUI based finder for searching the awesome resources on awesome series such as `awesome-python`, `awesome-go` and so on. 22 | 23 | With it, you can browse the awesome libraries, resources on your terminal without browser. 24 | 25 | [![asciicast](https://asciinema.org/a/OOdH9rLVBvReK3K6n7pZvruf9.png)](https://asciinema.org/a/OOdH9rLVBvReK3K6n7pZvruf9) 26 | 27 | ## Installation 28 | 29 | It supports **Python 3+** only. 30 | 31 | ```bash 32 | pip install awesome-finder # or pip3 install awesome-finder 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```console 38 | # Find awesome things from awesome-<topic> 39 | awesome-hub <topic> 40 | 41 | # Find awesome things from latest awesome-<topic> (not use cache) 42 | awesome-hub <topic> -f (--force) 43 | 44 | # Find awesome things with initial query 45 | awesome-hub <topic> -q (--query) 'query string you want to search' 46 | 47 | # Show help messages (can see supported awesome topics) 48 | awesome-hub -h (--help) 49 | ``` 50 | 51 | There are some helpful key bindings: 52 | 53 | | Key | Description | 54 | | ----------------- | ---------------------------------------- | 55 | | Key up (**↑**) | Scroll up | 56 | | Key down (**↓**) | Scroll down | 57 | | Key left (**←**) | Page up | 58 | | Key right (**→**) | Page down | 59 | | Enter (↵) | Open the selected awesome link on default browser | 60 | | Esc | Close the awesome finder | 61 | 62 | ## Supported awesome topics 63 | 64 | > *Updated: 2018-03-04* 65 | 66 | These will be updated continuously 67 | 68 | - awesome 69 | - awesome-android 70 | - awesome-elixir 71 | - awesome-erlang 72 | - awesome-go 73 | - awesome-ios 74 | - awesome-java 75 | - awesome-javascript 76 | - awesome-nodejs 77 | - awesome-php 78 | - awesome-python 79 | - awesome-ruby 80 | - awesome-rust 81 | - awesome-scala 82 | - awesome-swift 83 | - awesome-vue 84 | 85 | ## Contributing 86 | 87 | Details on [CONTRIBUTING](CONTRIBUTING.md) 88 | 89 | ## Changelog 90 | 91 | Details on [CHANGELOG](CHANGELOG.md) 92 | 93 | ## TODO 94 | 95 | * [ ] Query highlighting 96 | * [x] Supports paging with Key left (←) and Key right (→) 97 | * [ ] Smart parsing with hierachical structure 98 | * [ ] Supports all awesome series 99 | * [x] Supports initial query (example: `awesome python -q 'django oauth'`) 100 | * [ ] Add options to open the Issue and Pull Request page of a specific awesome series 101 | 102 | ## License 103 | 104 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fmingrammer%2Fawesome-finder.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fmingrammer%2Fawesome-finder?ref=badge_large) 105 | -------------------------------------------------------------------------------- /awesome/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | """parsers 2 | 3 | Retrieve a README file from awesome repository and parse it to get awesome list. 4 | """ 5 | import os 6 | import re 7 | import sys 8 | from abc import abstractmethod 9 | 10 | import requests 11 | 12 | from awesome.cache import exists, load_readme, save_readme 13 | 14 | sys.path.append(os.path.abspath(os.path.pardir)) 15 | 16 | 17 | class AbstractAwesomeParser(object): 18 | """An abstract parser class for providing the common methods for parsing the markdown based README 19 | 20 | Each Awesome README has slightly different formats, 21 | So should implements each parsers corresponding to each languages or topics 22 | """ 23 | AWESOME_TITLE = '' 24 | AWESOME_README_URL = '' 25 | 26 | def __init__(self, force_request=False): 27 | """Initialize the parser 28 | 29 | Args: 30 | force_request: If true, the parser does not use cached, but always request the README from remote repository 31 | """ 32 | self._markdown_header_regex = re.compile('#+\s+(.*)') 33 | self._markdown_link_line_regex = re.compile('\s?\[([^<>]+|(?:\[\w+\]))\]\s?\(([^\(\)]+)\)\s?(?::\w+:)?\s?(?:[-—]\s?(.*))?') 34 | self.force_request = force_request 35 | 36 | def is_cached(self): 37 | """Check if there exists cached README""" 38 | return exists(self.AWESOME_TITLE) 39 | 40 | def from_cache(self): 41 | """Retrieve the cached README from cache directory""" 42 | return load_readme(self.AWESOME_TITLE) 43 | 44 | def cache(self, readme): 45 | """Cache the readme to cache directory""" 46 | save_readme(self.AWESOME_TITLE, readme) 47 | 48 | def read_readme(self): 49 | """Read the README content from remote awesome repository""" 50 | if self.is_cached() and not self.force_request: 51 | print('Fetching the README from cache...') 52 | readme = self.from_cache() 53 | print('Done.') 54 | return readme 55 | print('Fetching the README from awesome place ...') 56 | response = requests.get(self.AWESOME_README_URL) 57 | print('Done.') 58 | if response.status_code == 200: 59 | readme = response.content.decode('utf8') 60 | self.cache(readme) 61 | return readme 62 | raise requests.RequestException('Error occurs when getting the README from {}'.format(self.AWESOME_README_URL)) 63 | 64 | def parse(self): 65 | """Parse the all content of README of Awesome repository""" 66 | content = self.find_content() 67 | awesome_blocks = self.parse_awesome_content(content) 68 | return awesome_blocks 69 | 70 | @abstractmethod 71 | def find_content(self): 72 | """Find only awesome content from README content""" 73 | raise NotImplementedError 74 | 75 | @abstractmethod 76 | def parse_awesome_content(self, content): 77 | """Parses the raw awesome content and returns a structured list as following form: 78 | 79 | The README is formed as follow: 80 | 81 | ------------------------------------ 82 | ... table of content ... <- TOC 83 | 84 | {separator between TOC and content} <- Separator such as '----' 85 | 86 | ## category <- Awesome content 87 | * [title](link) - description <- 88 | * [title](link) - description <- 89 | * [title](link) - description <- 90 | 91 | # others <- Useless parts 92 | * ... <- 93 | ------------------------------------ 94 | 95 | awesome_blocks = [ 96 | { 97 | 'type': <'category'|'awesome'>, 98 | 'line': 'line text', 99 | 'link'; [optional], 100 | }, 101 | ] 102 | 103 | Args 104 | content: All content of the awesome README 105 | """ 106 | raise NotImplementedError 107 | 108 | def parse_category_title(self, line): 109 | """Parse the header to get category title""" 110 | title = self._markdown_header_regex.findall(line)[0] 111 | plain_title = '[{}]'.format(title) 112 | return plain_title 113 | 114 | def parse_link_line(self, line): 115 | """Parse the line which has awesome link, title or description""" 116 | name, link, desc = self._markdown_link_line_regex.findall(line)[0] 117 | if desc: 118 | plain_line = '[{}] - {}'.format(name, desc) 119 | else: 120 | plain_line = '[{}]'.format(name) 121 | return plain_line, link 122 | -------------------------------------------------------------------------------- /awesome/tui.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | import curses 4 | import curses.textpad 5 | 6 | from textwrap import shorten 7 | 8 | 9 | class SearchScreen(object): 10 | """A window screen for searching the awesome list without browser""" 11 | UP = -1 12 | DOWN = 1 13 | 14 | query = '' 15 | 16 | def __init__(self, awesome_title, awesome_blocks, initial_query=''): 17 | """ Initialize the screen window 18 | 19 | Args 20 | awesome_title: Awesome topic title 21 | awesome_blocks: A list of formatted awesome content 22 | It has a set of names and web links for crawled awesome content 23 | initial_query: An optional initial query given from shell 24 | 25 | Attributes 26 | window: A full curses screen window 27 | result_window: A window for showing the results 28 | search_window: A window for search bar 29 | 30 | y: Current y coordinates 31 | 32 | width: The width of `window` 33 | height: The height of `window` 34 | 35 | awesome_title: Awesome title 36 | awesome_blocks: It holds given `awesome_blocks` argument 37 | matched_blocks: A list of found awesome content 38 | 39 | max_lines: Maximum visible line count for `result_window` 40 | top: Available top line position for current page (used on scrolling) 41 | bottom: Available bottom line position for whole pages (as length of found lines) 42 | current: Current highlighted line number 43 | page: Total page count which being changed dynamically corresponding to result of a query 44 | 45 | query: Query string for searching the awesome content 46 | 47 | Returns 48 | None 49 | """ 50 | self.window = None 51 | self.result_window = None 52 | self.search_window = None 53 | 54 | self.y = 0 55 | 56 | self.width = 0 57 | self.height = 0 58 | 59 | self.awesome_title = awesome_title 60 | self.awesome_blocks = awesome_blocks 61 | self.matched_blocks = awesome_blocks 62 | 63 | self.init_curses() 64 | self.init_layout() 65 | 66 | self.max_lines = curses.LINES - 4 67 | self.top = 0 68 | self.bottom = self.max_lines 69 | self.current = 0 70 | self.page = self.bottom // self.max_lines 71 | 72 | self.write_string(initial_query) 73 | 74 | def init_curses(self): 75 | """Setup the curses""" 76 | self.window = curses.initscr() 77 | self.window.keypad(True) 78 | 79 | curses.noecho() 80 | curses.cbreak() 81 | 82 | curses.start_color() 83 | curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK) 84 | curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_CYAN) 85 | 86 | self.current = curses.color_pair(2) 87 | 88 | def init_layout(self): 89 | """Initialize the each windows with their size and shape""" 90 | self.height, self.width = self.window.getmaxyx() 91 | 92 | # Title section 93 | self.window.addstr(0, 0, '[awesome-{}] Find awesome things!'.format(self.awesome_title), curses.color_pair(1)) 94 | self.window.hline(1, 0, curses.ACS_HLINE, self.width) 95 | 96 | # Search result section 97 | self.result_window = curses.newwin(self.height - 4, self.width, 2, 0) 98 | self.result_window.keypad(True) 99 | 100 | # Search bar section 101 | self.window.hline(self.height - 2, 0, curses.ACS_HLINE, self.width) 102 | self.window.addch(self.height - 1, 0, '>') 103 | self.search_window = curses.newwin(1, self.width - 1, self.height - 1, 2) 104 | self.search_window.keypad(True) 105 | 106 | self.window.refresh() 107 | 108 | def reset_top(self): 109 | """Reset the top 110 | 111 | It is used to move up the scroll page to top position for next query 112 | """ 113 | self.top = 0 114 | 115 | def run(self): 116 | """Continue running the awesome finder until get interrupted""" 117 | try: 118 | self.input_stream() 119 | except KeyboardInterrupt: 120 | pass 121 | finally: 122 | curses.endwin() 123 | 124 | def input_stream(self): 125 | """Waiting an input and run a proper method according to type of input""" 126 | while True: 127 | self.search(self.query) 128 | self.display() 129 | 130 | ch = self.search_window.getch() 131 | if curses.ascii.isprint(ch): 132 | self.write(ch) 133 | self.reset_top() 134 | elif ch in (curses.ascii.BS, curses.ascii.DEL, curses.KEY_BACKSPACE): 135 | self.delete() 136 | self.reset_top() 137 | elif ch == curses.KEY_UP: 138 | self.scroll(self.UP) 139 | elif ch == curses.KEY_DOWN: 140 | self.scroll(self.DOWN) 141 | elif ch == curses.KEY_LEFT: 142 | self.paging(self.UP) 143 | elif ch == curses.KEY_RIGHT: 144 | self.paging(self.DOWN) 145 | elif ch in (curses.ascii.LF, curses.ascii.NL): 146 | self.open_link() 147 | elif ch == curses.ascii.ESC: 148 | break 149 | 150 | def write(self, ch): 151 | """Write a character and append it to the query""" 152 | self.search_window.addch(ch) 153 | self.query += chr(ch) 154 | 155 | def write_string(self, string): 156 | """Write a string""" 157 | for ch in string: 158 | self.write(ord(ch)) 159 | 160 | def delete(self): 161 | """Delete an ending character""" 162 | self.y, _ = self.search_window.getyx() 163 | if len(self.query) > 0: 164 | self.search_window.move(self.y, len(self.query) - 1) 165 | self.search_window.delch() 166 | self.query = self.query[:-1] 167 | 168 | def scroll(self, direction): 169 | """Scrolling the result window when pressing up/down arrow keys 170 | 171 | Args: 172 | direction: Up or Down 173 | 174 | Returns: 175 | None 176 | """ 177 | next_line = self.current + direction 178 | 179 | # Up direction scroll overflow 180 | if (direction == self.UP) and (self.top > 0 and self.current == 0): 181 | self.top += direction 182 | return 183 | # Down direction scroll overflow 184 | if (direction == self.DOWN) and (next_line == self.max_lines) and (self.top + self.max_lines < self.bottom): 185 | self.top += direction 186 | return 187 | # Scroll up 188 | if (direction == self.UP) and (self.top > 0 or self.current > 0): 189 | self.current = next_line 190 | return 191 | # Scroll down 192 | if (direction == self.DOWN) and (next_line < self.max_lines) and (self.top + next_line < self.bottom): 193 | self.current = next_line 194 | return 195 | 196 | def paging(self, direction): 197 | """Paging the result window when pressing left/right arrow keys 198 | 199 | Args: 200 | direction: Up or Down 201 | 202 | Returns: 203 | None 204 | """ 205 | current_page = (self.top + self.current) // self.max_lines 206 | next_page = current_page + direction 207 | # The last page may have fewer items than max lines, 208 | # so we should adjust the current cursor position as maximum item count on last page 209 | if next_page == self.page: 210 | self.current = min(self.current, self.bottom % self.max_lines - 1) 211 | 212 | # Page up 213 | if (direction == self.UP) and (current_page > 0): 214 | self.top = max(0, self.top - self.max_lines) 215 | return 216 | # Page down 217 | if (direction == self.DOWN) and (current_page < self.page): 218 | self.top += self.max_lines 219 | return 220 | 221 | def open_link(self): 222 | """Open the link of highlighted awesome content""" 223 | index = self.top + self.current 224 | if self.matched_blocks and self.matched_blocks[index]['type'] == 'awesome': 225 | webbrowser.open(self.matched_blocks[index]['link'], new=2) 226 | 227 | def search(self, query): 228 | """Search the awesome content with given query 229 | 230 | Args: 231 | query: Query string 232 | 233 | Returns: 234 | None 235 | """ 236 | matched_blocks = [] 237 | if query: 238 | query_tokens = query.lower().split() 239 | for block in self.awesome_blocks: 240 | if all([token in block['line'].lower() for token in query_tokens]): 241 | matched_blocks.append(block) 242 | else: 243 | matched_blocks = self.awesome_blocks 244 | 245 | # Set the bottom to length of matched blocks 246 | self.bottom = len(matched_blocks) 247 | self.page = self.bottom // self.max_lines 248 | 249 | # When reached bottom line, stop scrolling 250 | if self.current > self.bottom: 251 | self.current = self.bottom - 1 252 | self.matched_blocks = matched_blocks 253 | 254 | def display(self): 255 | """Display the found awesome content on result window""" 256 | self.result_window.erase() 257 | for idx, val in enumerate(self.matched_blocks[self.top:self.top + self.max_lines]): 258 | if val['type'] == 'category': 259 | # Highlight the current cursor line 260 | if idx == self.current: 261 | self.result_window.addstr(idx, 0, shorten(val['line'], self.width, placeholder='...'), 262 | curses.color_pair(2)) 263 | else: 264 | self.result_window.addstr(idx, 0, shorten(val['line'], self.width, placeholder='...'), 265 | curses.color_pair(1)) 266 | elif val['type'] == 'awesome': 267 | # Highlight the current cursor line 268 | if idx == self.current: 269 | self.result_window.addstr(idx, 2, shorten(val['line'], self.width - 3, placeholder='...'), 270 | curses.color_pair(2)) 271 | else: 272 | self.result_window.addstr(idx, 2, shorten(val['line'], self.width - 3, placeholder='...')) 273 | self.result_window.refresh() 274 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "96a6d7fb4615e1bef2dc5c5bad0cd7147512ad6eddbb06c967d9c11fb0baad6d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 22 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 23 | ], 24 | "version": "==2020.12.5" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "idna": { 34 | "hashes": [ 35 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 36 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 37 | ], 38 | "version": "==2.8" 39 | }, 40 | "requests": { 41 | "hashes": [ 42 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 43 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 44 | ], 45 | "index": "pypi", 46 | "version": "==2.22.0" 47 | }, 48 | "urllib3": { 49 | "hashes": [ 50 | "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", 51 | "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" 52 | ], 53 | "version": "==1.25.11" 54 | } 55 | }, 56 | "develop": { 57 | "bleach": { 58 | "hashes": [ 59 | "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080", 60 | "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd" 61 | ], 62 | "version": "==3.2.1" 63 | }, 64 | "certifi": { 65 | "hashes": [ 66 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 67 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 68 | ], 69 | "version": "==2020.12.5" 70 | }, 71 | "cffi": { 72 | "hashes": [ 73 | "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e", 74 | "sha256:00e28066507bfc3fe865a31f325c8391a1ac2916219340f87dfad602c3e48e5d", 75 | "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a", 76 | "sha256:0638c3ae1a0edfb77c6765d487fee624d2b1ee1bdfeffc1f0b58c64d149e7eec", 77 | "sha256:105abaf8a6075dc96c1fe5ae7aae073f4696f2905fde6aeada4c9d2926752362", 78 | "sha256:155136b51fd733fa94e1c2ea5211dcd4c8879869008fc811648f16541bf99668", 79 | "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c", 80 | "sha256:1d2c4994f515e5b485fd6d3a73d05526aa0fcf248eb135996b088d25dfa1865b", 81 | "sha256:2c24d61263f511551f740d1a065eb0212db1dbbbbd241db758f5244281590c06", 82 | "sha256:51a8b381b16ddd370178a65360ebe15fbc1c71cf6f584613a7ea08bfad946698", 83 | "sha256:594234691ac0e9b770aee9fcdb8fa02c22e43e5c619456efd0d6c2bf276f3eb2", 84 | "sha256:5cf4be6c304ad0b6602f5c4e90e2f59b47653ac1ed9c662ed379fe48a8f26b0c", 85 | "sha256:64081b3f8f6f3c3de6191ec89d7dc6c86a8a43911f7ecb422c60e90c70be41c7", 86 | "sha256:6bc25fc545a6b3d57b5f8618e59fc13d3a3a68431e8ca5fd4c13241cd70d0009", 87 | "sha256:798caa2a2384b1cbe8a2a139d80734c9db54f9cc155c99d7cc92441a23871c03", 88 | "sha256:7c6b1dece89874d9541fc974917b631406233ea0440d0bdfbb8e03bf39a49b3b", 89 | "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909", 90 | "sha256:8d6603078baf4e11edc4168a514c5ce5b3ba6e3e9c374298cb88437957960a53", 91 | "sha256:9cc46bc107224ff5b6d04369e7c595acb700c3613ad7bcf2e2012f62ece80c35", 92 | "sha256:9f7a31251289b2ab6d4012f6e83e58bc3b96bd151f5b5262467f4bb6b34a7c26", 93 | "sha256:9ffb888f19d54a4d4dfd4b3f29bc2c16aa4972f1c2ab9c4ab09b8ab8685b9c2b", 94 | "sha256:a5ed8c05548b54b998b9498753fb9cadbfd92ee88e884641377d8a8b291bcc01", 95 | "sha256:a7711edca4dcef1a75257b50a2fbfe92a65187c47dab5a0f1b9b332c5919a3fb", 96 | "sha256:af5c59122a011049aad5dd87424b8e65a80e4a6477419c0c1015f73fb5ea0293", 97 | "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd", 98 | "sha256:b4e248d1087abf9f4c10f3c398896c87ce82a9856494a7155823eb45a892395d", 99 | "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3", 100 | "sha256:c6332685306b6417a91b1ff9fae889b3ba65c2292d64bd9245c093b1b284809d", 101 | "sha256:d5ff0621c88ce83a28a10d2ce719b2ee85635e85c515f12bac99a95306da4b2e", 102 | "sha256:d9efd8b7a3ef378dd61a1e77367f1924375befc2eba06168b6ebfa903a5e59ca", 103 | "sha256:df5169c4396adc04f9b0a05f13c074df878b6052430e03f50e68adf3a57aa28d", 104 | "sha256:ebb253464a5d0482b191274f1c8bf00e33f7e0b9c66405fbffc61ed2c839c775", 105 | "sha256:ec80dc47f54e6e9a78181ce05feb71a0353854cc26999db963695f950b5fb375", 106 | "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b", 107 | "sha256:f60567825f791c6f8a592f3c6e3bd93dd2934e3f9dac189308426bd76b00ef3b", 108 | "sha256:f803eaa94c2fcda012c047e62bc7a51b0bdabda1cad7a92a522694ea2d76e49f" 109 | ], 110 | "version": "==1.14.4" 111 | }, 112 | "chardet": { 113 | "hashes": [ 114 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 115 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 116 | ], 117 | "version": "==3.0.4" 118 | }, 119 | "cryptography": { 120 | "hashes": [ 121 | "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d", 122 | "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832", 123 | "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98", 124 | "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d", 125 | "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a", 126 | "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943", 127 | "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917", 128 | "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f", 129 | "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6", 130 | "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a", 131 | "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3", 132 | "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831", 133 | "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af", 134 | "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5", 135 | "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5", 136 | "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591", 137 | "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a", 138 | "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9", 139 | "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c", 140 | "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae", 141 | "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f", 142 | "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef" 143 | ], 144 | "index": "pypi", 145 | "version": "==3.2" 146 | }, 147 | "docutils": { 148 | "hashes": [ 149 | "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", 150 | "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" 151 | ], 152 | "version": "==0.16" 153 | }, 154 | "idna": { 155 | "hashes": [ 156 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 157 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 158 | ], 159 | "version": "==2.8" 160 | }, 161 | "importlib-metadata": { 162 | "hashes": [ 163 | "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed", 164 | "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450" 165 | ], 166 | "markers": "python_version < '3.8'", 167 | "version": "==3.3.0" 168 | }, 169 | "jeepney": { 170 | "hashes": [ 171 | "sha256:7d59b6622675ca9e993a6bd38de845051d315f8b0c72cca3aef733a20b648657", 172 | "sha256:aec56c0eb1691a841795111e184e13cad504f7703b9a64f63020816afa79a8ae" 173 | ], 174 | "markers": "sys_platform == 'linux'", 175 | "version": "==0.6.0" 176 | }, 177 | "keyring": { 178 | "hashes": [ 179 | "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675", 180 | "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea" 181 | ], 182 | "version": "==21.8.0" 183 | }, 184 | "packaging": { 185 | "hashes": [ 186 | "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858", 187 | "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093" 188 | ], 189 | "version": "==20.8" 190 | }, 191 | "pkginfo": { 192 | "hashes": [ 193 | "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193", 194 | "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9" 195 | ], 196 | "version": "==1.6.1" 197 | }, 198 | "pycparser": { 199 | "hashes": [ 200 | "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0", 201 | "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705" 202 | ], 203 | "version": "==2.20" 204 | }, 205 | "pygments": { 206 | "hashes": [ 207 | "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716", 208 | "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08" 209 | ], 210 | "version": "==2.7.3" 211 | }, 212 | "pyparsing": { 213 | "hashes": [ 214 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 215 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 216 | ], 217 | "version": "==2.4.7" 218 | }, 219 | "readme-renderer": { 220 | "hashes": [ 221 | "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d", 222 | "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a" 223 | ], 224 | "version": "==28.0" 225 | }, 226 | "requests": { 227 | "hashes": [ 228 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 229 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 230 | ], 231 | "index": "pypi", 232 | "version": "==2.22.0" 233 | }, 234 | "requests-toolbelt": { 235 | "hashes": [ 236 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 237 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 238 | ], 239 | "version": "==0.9.1" 240 | }, 241 | "secretstorage": { 242 | "hashes": [ 243 | "sha256:30cfdef28829dad64d6ea1ed08f8eff6aa115a77068926bcc9f5225d5a3246aa", 244 | "sha256:5c36f6537a523ec5f969ef9fad61c98eb9e017bc601d811e53aa25bece64892f" 245 | ], 246 | "markers": "sys_platform == 'linux'", 247 | "version": "==3.3.0" 248 | }, 249 | "six": { 250 | "hashes": [ 251 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 252 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 253 | ], 254 | "version": "==1.15.0" 255 | }, 256 | "tqdm": { 257 | "hashes": [ 258 | "sha256:0cd81710de29754bf17b6fee07bdb86f956b4fa20d3078f02040f83e64309416", 259 | "sha256:f4f80b96e2ceafea69add7bf971b8403b9cba8fb4451c1220f91c79be4ebd208" 260 | ], 261 | "version": "==4.55.0" 262 | }, 263 | "twine": { 264 | "hashes": [ 265 | "sha256:c1af8ca391e43b0a06bbc155f7f67db0bf0d19d284bfc88d1675da497a946124", 266 | "sha256:d561a5e511f70275e5a485a6275ff61851c16ffcb3a95a602189161112d9f160" 267 | ], 268 | "index": "pypi", 269 | "version": "==3.1.1" 270 | }, 271 | "typing-extensions": { 272 | "hashes": [ 273 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 274 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 275 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 276 | ], 277 | "markers": "python_version < '3.8'", 278 | "version": "==3.7.4.3" 279 | }, 280 | "urllib3": { 281 | "hashes": [ 282 | "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", 283 | "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" 284 | ], 285 | "version": "==1.25.11" 286 | }, 287 | "webencodings": { 288 | "hashes": [ 289 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 290 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 291 | ], 292 | "version": "==0.5.1" 293 | }, 294 | "zipp": { 295 | "hashes": [ 296 | "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108", 297 | "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb" 298 | ], 299 | "version": "==3.4.0" 300 | } 301 | } 302 | } 303 | --------------------------------------------------------------------------------