├── .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 |
--------------------------------------------------------------------------------