├── .gitignore ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py └── stackit ├── __init__.py └── stackit_core.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lukas Schwab 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stackit 2 | 3 | `stackit` is a Stack Overflow query CLI built at SB Hacks 2015. 4 | 5 | ## Features 6 | 7 | + Written entirely in Python 8 | + Automatically pipes error messages into Stack Overflow queries 9 | + Parses and displays relevant questions and answers directly on the command line in reader-friendly markdown format 10 | 11 | ## Installation 12 | 13 | There are two ways to install stackit. Both should have roughly the same outcome, but have their advantages/disadvantages. 14 | 15 | **1. PyPI/pip** 16 | 17 | This method will always produce some stable build, but may not be the most up to date version. New functionality will come slower than building from this repo. 18 | 19 | $ pip install stackit 20 | 21 | Note, depending on your computer's settings, you may need to `sudo pip install stackit`. 22 | 23 | **2. Build from this repo** 24 | 25 | This method will always include the latest features, but sometimes will not work at all. Oops! 26 | 27 | Clone the repo, then use setup.py to install the package. Note, this process will differ only slightly in a non-bash shell. 28 | 29 | $ git clone https://github.com/lukasschwab/stackit.git 30 | $ cd stackit 31 | $ python setup.py install 32 | 33 | Note, depending on your computer's settings, you may need to `sudo python setup.py install`. 34 | 35 | ## Usage 36 | 37 | The install process establishes an alias, `stackit`, for stackit_core.py's functionality. Instead of using `python stackit_core.py`, you will *always* simply use `stackit` at the command prompt. 38 | 39 |

40 | 41 | ### Command line arguments 42 | + `-h`, `--help`: version splash page // usage 43 | + `-s`: `--search`: search by user term (string) 44 | + `--version`: simple version report 45 | + `--verbose`: full text of top result and accepted answer 46 | + `-e`: `--stderr`: runs your program and searches by stderr output 47 | + `-t`: `--tags`: searches by tags in particular (multiple arguments) 48 | 49 | ### Interface flow commands 50 | + `m`: more: shows the next 5 questions 51 | + #: select: shows full question//top answer text in focus -- be careful that it's clearly not the SO question ID, but the list index 52 | + `--b`: opens focused question in browser 53 | + `--x`: exit: go back to the list focus 54 | 55 | ### Examples 56 | To search Stack Overflow for "How do I create a bash alias" with the tags, "shell"; 57 | 58 | `$ stackit -s "How do I create a bash alias?" -t "shell"` 59 | 60 | ## Thanks 61 | 62 | `stackit` uses several pre-existing projects: 63 | 64 | + [Py-StackExchange](https://github.com/lucjon/Py-StackExchange): a Python wrapper for the StackExchange API 65 | + [Requests](https://github.com/kennethreitz/requests): "HTTP for Humans" 66 | + [Beautiful Soup 4](http://www.crummy.com/software/BeautifulSoup/bs4/doc/): pretty data parsing for HTML/XML files, so you can read stuff 67 | + [Pyfancy](https://github.com/ilovecode1/pyfancy) makes your print statements colorful // legible. This project doesn't incorporate `pyfancy.py` verbatim, but this project demonstrates the method. 68 | + [Karan Goel](https://github.com/karan)'s work with [joe](https://github.com/karan/joe) was a tremendous help in designing a command line tool in Python, and his [Medium article](https://medium.com/@karan/these-6-simple-changes-made-my-recent-side-project-go-viral-53fd6571c11c) on putting together a good readme is an inspiration to us all. Thank you based joe. 69 | + Peter Downs has a great [article](http://peterdowns.com/posts/first-time-with-pypi.html) on how to submit a package to PyPI. 70 | 71 | ## Contributing 72 | 73 | If you want to write code: 74 | 75 | 1. Fork the repository 76 | 2. Create your feature branch (`git checkout -b my-new-feature`) 77 | 3. Commit your changes (`git commit -am 'add some feature'`) 78 | 4. Push to your branch (`git push origin my-new-feature`) 79 | 5. Create a new Pull Request 80 | 81 | ### SB Hacks 2015 team 82 | 83 | + [Vicki Niu](https://github.com/vickiniu) 84 | + [Leilani Reyes](https://github.com/lanidelrey) 85 | + [Eni Asebiomo](https://github.com/eniasebiomo) 86 | + [Lukas Schwab](https://github.com/lukasschwab) 87 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup( 5 | name="stackit", 6 | version="0.1.7", 7 | packages=['stackit'], 8 | 9 | # dependencies 10 | install_requires=[ 11 | 'Py-StackExchange', 12 | 'html2text', 13 | 'Click', 14 | ], 15 | 16 | # metadata for upload to PyPI 17 | author="SB Hacks Crew", 18 | author_email="lukas.schwab@gmail.com", 19 | description="stackit sends smart StackOverflow queries from your command line", 20 | license="MIT", 21 | keywords="error stderr stack overflow stackoverflow stack exchange stackexchange", 22 | url="https://github.com/lukasschwab/stackit", # project homepage 23 | download_url="https://github.com/lukasschwab/stackit/tarball/0.1.7", 24 | 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'stackit=stackit.stackit_core:main' 28 | ] 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /stackit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasschwab/stackit/2e61cf0acdf41244c3e3f8983e7b871c8f177d1c/stackit/__init__.py -------------------------------------------------------------------------------- /stackit/stackit_core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | from __future__ import unicode_literals 4 | import sys 5 | import stackexchange 6 | from stackexchange import Sort 7 | # A good testing URL: http://stackoverflow.com/questions/16800049/changepassword-test 8 | # The approved answer ID: 16800090 9 | 10 | import subprocess 11 | import click 12 | import os 13 | 14 | 15 | if sys.version_info[:2] < (3, 0): 16 | input = raw_input 17 | 18 | NUM_RESULTS = 5 19 | # API key is public, according to SO documentation 20 | # (link?) 21 | API_KEY = "3GBT2vbKxgh*ati7EBzxGA((" 22 | VERSION_NUM = "0.1.7" 23 | 24 | # HTML to markdown parsing 25 | # https://github.com/aaronsw/html2text 26 | import html2text 27 | h = html2text.HTML2Text() 28 | 29 | user_api_key = API_KEY 30 | 31 | so = stackexchange.Site(stackexchange.StackOverflow, app_key=user_api_key, impose_throttling=True) 32 | so.be_inclusive() 33 | 34 | 35 | class Config(): 36 | """ Main configuration object """ 37 | def __init__(self): 38 | self.search = False 39 | self.stderr = False 40 | self.tag = False 41 | self.verbose = False 42 | 43 | 44 | pass_config = click.make_pass_decorator(Config, ensure=True) 45 | 46 | 47 | def select(questions, num): 48 | print_full_question(questions[num - 1]) 49 | working = True 50 | while working: 51 | user_input = click.prompt("Enter b to launch browser, x to return to search, or q to quit") 52 | if user_input == 'b': 53 | click.launch(questions[num - 1].json['link']) 54 | elif user_input == 'q': 55 | sys.exit() 56 | elif user_input == 'x': 57 | click.echo("\n" * 12) 58 | # Ranging over the 5 questions including the user's choice 59 | origin = 0 60 | if not num % NUM_RESULTS: 61 | origin = num - NUM_RESULTS 62 | else: 63 | origin = num - num % NUM_RESULTS 64 | for j in range(origin, origin + NUM_RESULTS): 65 | print_question(questions[j], j + 1) 66 | working = False 67 | else: 68 | click.echo(click.style( 69 | "The input entered was not recognized as a valid choice.", 70 | fg="red", 71 | err=True)) 72 | 73 | 74 | def focus_question(questions): 75 | working = True 76 | while working: 77 | user_input = click.prompt("Enter m for more, a question number to select, or q to quit") 78 | if user_input == 'm': 79 | working = False 80 | elif user_input == 'q': 81 | sys.exit() 82 | elif user_input.isnumeric() and int(user_input) <= len(questions): 83 | select(questions, int(user_input)) 84 | else: 85 | click.echo(click.style( 86 | "The input entered was not recognized as a valid choice.", 87 | fg="red", 88 | err=True)) 89 | 90 | 91 | def _search(config): 92 | # inform user 93 | click.echo('Searching for: {0}...'.format(config.term)) 94 | click.echo('Tags: {0}'.format(config.tag)) 95 | 96 | questions = so.search_advanced( 97 | q=config.term, 98 | tagged=config.tag.split(), 99 | sort=Sort.Votes) 100 | 101 | count = 0 102 | question_logs = [] 103 | # quicker way for appending to list 104 | add_to_logs = question_logs.append 105 | for question in questions: 106 | if 'accepted_answer_id' in question.json: 107 | count += 1 108 | add_to_logs(question) 109 | print_question(question, count) 110 | if count % NUM_RESULTS == 0: 111 | focus_question(question_logs) 112 | 113 | if not questions: 114 | click.echo( 115 | click.style( 116 | "Your search \'{0}\' with tags \'{1}\' returned no results.".format(config.term, config.tag), 117 | fg="red", 118 | err=True)) 119 | sys.exit(1) 120 | 121 | 122 | def print_question(question, count): 123 | answerid = question.json['accepted_answer_id'] 124 | 125 | answer = h.handle(so.answer(answerid, body=True).body) 126 | # only 140 first char, tweet like answer 127 | if len(answer) > 140: 128 | answer = ''.join([answer[:140], '...']) 129 | 130 | click.echo(''.join([ 131 | click.style(''.join([str(count), '\nQuestion: ', question.title]), fg='blue'), 132 | ''.join(['\nAnswer:', answer, "\n"]), 133 | ])) 134 | 135 | 136 | def get_term(config): 137 | if config.search: 138 | return config.search 139 | elif config.stderr: 140 | # don't show stdout to user 141 | with open(os.devnull, 'wb') as DEVNULL: 142 | process = subprocess.Popen( 143 | config.stderr, 144 | stdout=DEVNULL, 145 | stderr=subprocess.PIPE, shell=True) 146 | 147 | output = process.communicate()[1].splitlines() 148 | 149 | # abort if no error 150 | if not len(output): 151 | click.echo(click.style( 152 | "Your executable does not raise an error.", 153 | fg="red")) 154 | sys.exit(1) 155 | 156 | return str(output[-1]) 157 | return "" 158 | 159 | 160 | def print_full_question(question): 161 | answerid = question.json['accepted_answer_id'] 162 | 163 | questiontext = h.handle(so.question(question.id, body=True).body) 164 | answer = h.handle(so.answer(answerid, body=True).body) 165 | 166 | click.echo(''.join([ 167 | click.style(''.join([ 168 | "\n\n------------------------------QUESTION-----------------------------------\n\n", 169 | question.title, '\n', questiontext, 170 | ]), fg='blue'), 171 | ''.join([ 172 | "\n-------------------------------ANSWER------------------------------------", 173 | answer, 174 | ]), 175 | ])) 176 | 177 | 178 | def search_verbose(term): 179 | questions = so.search_advanced(q=term, sort=Sort.Votes) 180 | question = questions[0] 181 | print_full_question(question) 182 | 183 | 184 | @click.command() 185 | @click.option("-s", "--search", default="", help="Searches StackOverflow for your query") 186 | @click.option("-e", "--stderr", default="", help="Runs an executable command (i.e. python script.py) and automatically inputs error message to StackOverflow") 187 | @click.option("-t", "--tag", default="", help="Searches StackOverflow for your tags") 188 | @click.option("--verbose", is_flag=True, help="displays full text of most relevant question and answer") 189 | @click.option("--version", is_flag=True, help="displays the version") 190 | @pass_config 191 | def main(config, search, stderr, tag, verbose, version): 192 | """ Parses command-line arguments for StackIt """ 193 | config.search = search 194 | config.stderr = stderr 195 | config.tag = tag 196 | config.verbose = verbose 197 | 198 | config.term = get_term(config) 199 | 200 | if verbose: 201 | search_verbose(config.term) 202 | elif search or stderr: 203 | _search(config) 204 | elif version: 205 | click.echo("Version {VERSION_NUM}".format(**globals())) 206 | else: 207 | click.echo( 208 | click.style( 209 | "No argument provided, use --help for help", 210 | fg="red", 211 | err=True)) 212 | sys.exit(1) 213 | 214 | if __name__ == '__main__': 215 | main() 216 | --------------------------------------------------------------------------------