11from termcolor import colored
22import requests
33from rich .console import Console
4+ from rich .markdown import Markdown
45import sys as sys
6+
7+ # Required for Questions Panel
58import os
69import time
710from collections import defaultdict
11+ from simple_term_menu import TerminalMenu
12+ import webbrowser
813
914from .error import SearchError
1015from .save import SaveSearchResults
2833
2934console = Console ()
3035
36+ class QuestionsPanelStackoverflow ():
37+ def __init__ (self ):
38+ self .questions_data = [] # list( list( question_title, question_id, question_link )... )
39+ self .answer_data = defaultdict (lambda : False ) # dict( question_id:list( body, link )) corresponding to self.questions_data
40+ self .line_color = "bold red"
41+ self .heading_color = "bold blue"
42+ self .utility = Utility ()
43+
44+ def populate_question_data (self , questions_list ):
45+ """
46+ Function to populate question data property
47+ Creates batch request to stackexchange API and to get question details of
48+ questions with id in the list. Stores the returned data data in the following format:
49+ list( list( question_title, question_link, question_id ) )
50+ """
51+ with console .status ("Getting the questions..." ):
52+ try :
53+ resp = requests .get (
54+ self .utility .get_batch_ques_url (questions_list )
55+ )
56+ except :
57+ SearchError ("Search Failed" , "Try connecting to the internet" )
58+ sys .exit ()
59+ json_ques_data = resp .json ()
60+ self .questions_data = [[item ['title' ].replace ('|' ,'' ), item ['question_id' ], item ['link' ]] for item in json_ques_data ["items" ]]
61+
62+ def populate_answer_data (self , questions_list ):
63+ """
64+ Function to populate answer data property
65+ Creates batch request to stackexchange API to get ans of questions with
66+ question id in the list. Stores the returned data data in the following format:
67+ dict( question_id:list( body, link ) )
68+ """
69+ with console .status ("Searching answers..." ):
70+ try :
71+ resp = requests .get (
72+ self .utility .get_batch_ans_url (questions_list )
73+ )
74+ except :
75+ SearchError ("Search Failed" , "Try connecting to the internet" )
76+ sys .exit ()
77+ json_ans_data = resp .json ()
78+ for item in json_ans_data ["items" ]:
79+ if not (self .answer_data [item ['question_id' ]]):
80+ self .answer_data [item ['question_id' ]] = item ['body_markdown' ]
81+ # Sometimes the StackExchange API fails to deliver some answers. The below code is to fetch them
82+ failed_ques_id = [question [1 ] for question in self .questions_data if not (self .answer_data [question [1 ]])]
83+ if not (len (failed_ques_id ) == 0 ):
84+ self .populate_answer_data (failed_ques_id )
85+
86+ def return_formatted_ans (self , ques_id ):
87+ # This function uses pygments lexers ad formatters to format the content in the preview screen
88+ body_markdown = self .answer_data [int (ques_id )]
89+ body_markdown = str (body_markdown )
90+ xml_markup_replacement = [("&" , "&" ), ("<" , "<" ), (">" , ">" ), (""" , "\" " ), ("'" , "\' " ), ("'" , "\' " )]
91+ for convert_from , convert_to in xml_markup_replacement :
92+ body_markdown = body_markdown .replace (convert_from , convert_to )
93+ width = os .get_terminal_size ().columns
94+ console = Console (width = width - 4 )
95+ markdown = Markdown (body_markdown , hyperlinks = False )
96+ with console .capture () as capture :
97+ console .print (markdown )
98+ highlighted = capture .get ()
99+ box_replacement = [("─" , "-" ), ("═" ,"=" ), ("║" ,"|" ), ("│" , "|" ), ('┌' , '+' ), ("└" , "+" ), ("┐" , "+" ), ("┘" , "+" ), ("╔" , "+" ), ("╚" , "+" ), ("╗" ,"+" ), ("╝" , "+" ), ("•" ,"*" )]
100+ for convert_from , convert_to in box_replacement :
101+ highlighted = highlighted .replace (convert_from , convert_to )
102+ return highlighted
103+
104+ def navigate_questions_panel (self ):
105+ # Code for navigating through the question panel
106+ console .rule ('[bold blue] Relevant Questions' , style = "bold red" )
107+ console .print ("[yellow] Use arrow keys to navigate. 'q' or 'Esc' to quit. 'Enter' to open in a browser" )
108+ console .print ()
109+ options = ["|" .join (map (str , question )) for question in self .questions_data ]
110+ question_menu = TerminalMenu (options , preview_command = self .return_formatted_ans , preview_size = 0.75 , )
111+ quitting = False
112+ while not (quitting ):
113+ options_index = question_menu .show ()
114+ try :
115+ question_link = self .questions_data [options_index ][2 ]
116+ except Exception :
117+ return
118+ else :
119+ webbrowser .open (question_link )
120+
121+ def display_panel (self , questions_list ):
122+ self .populate_question_data (questions_list )
123+ self .populate_answer_data (questions_list )
124+ self .navigate_questions_panel ()
31125
32126class Utility ():
33127 def __init__ (self ):
@@ -42,6 +136,22 @@ def __get_search_url(self, question, tags):
42136 """
43137 return f"{ self .search_content_url } /2.2/search/advanced?order=desc&sort=relevance&tagged={ tags } &title={ question } &site=stackoverflow"
44138
139+ def get_batch_ques_url (self , ques_id_list ):
140+ """
141+ Returns URL which contains ques_ids which can be use to get
142+ get the details of all the corresponding questions
143+ """
144+ batch_ques_id = ""
145+ for question_id in ques_id_list :
146+ batch_ques_id += str (question_id ) + ";"
147+ return f"{ self .search_content_url } /2.2/questions/{ batch_ques_id [:- 1 ]} ?order=desc&sort=votes&site=stackoverflow&filter=!--1nZwsgqvRX"
148+
149+ def get_batch_ans_url (self , ques_id_list ):
150+ batch_ques_id = ""
151+ for question_id in ques_id_list :
152+ batch_ques_id += str (question_id ) + ";"
153+ return f"{ self .search_content_url } /2.2/questions/{ batch_ques_id [:- 1 ]} /answers?order=desc&sort=votes&site=stackoverflow&filter=!--1nZwsgqvRX"
154+
45155 def make_request (self , que , tag : str ):
46156 """
47157 This function uses the requests library to make the rest api call to the stackexchange server.
@@ -52,12 +162,12 @@ def make_request(self, que, tag: str):
52162 :return: Json response from the api call.
53163 :rtype: Json format data
54164 """
55- print ( " \U0001F50E Searching for the answer" )
56- try :
57- resp = requests .get (self .__get_search_url (que , tag ))
58- except :
59- SearchError ("\U0001F613 Search Failed" , "\U0001F4BB Try connecting to the internet" )
60- sys .exit ()
165+ with console . status ( " Searching..." ):
166+ try :
167+ resp = requests .get (self .__get_search_url (que , tag ))
168+ except :
169+ SearchError ("\U0001F613 Search Failed" , "\U0001F4BB Try connecting to the internet" )
170+ sys .exit ()
61171 return resp .json ()
62172
63173 def get_que (self , json_data ):
@@ -74,105 +184,12 @@ def get_que(self, json_data):
74184
75185 def get_ans (self , questions_list ):
76186 """
77- This function prints the answer to the queries
78- (question and tags) provided by the user. It does so
79- in the following manner :
80- 1) Gets the details of all the relevant question and stores their title, link and id in "question_data" list.
81- 2) I have introduced the concept of active question, i.e. , the question whose answer is currently being displayed.
82- 3) The index of the active question in "question_data" array is stored in "question_posx"
83- 2) By Default, shows the answer to the first question. Creates an breakable infinite loop asking the user answer to which question he wants to see.
84- 4) The value of "question_posx" changes according to user input, displaying answer to different questions in "questions_data" list.
85- 3) The answers to the questions requested by the user are stored in cache for faster access time during subsequent calls.
86- """
87- # Create batch request to get details of all the questions
88- batch_ques_id = ""
89- for question_id in questions_list :
90- batch_ques_id += str (question_id ) + ";"
91- try :
92- resp = requests .get (
93- f"{ self .search_content_url } /2.2/questions/{ batch_ques_id [:- 1 ]} ?order=desc&sort=votes&site=stackoverflow&filter=!--1nZwsgqvRX"
94- )
95- except :
96- SearchError ("Search Failed" , "Try connecting to the internet" )
97- sys .exit ()
98- json_ques_data = resp .json ()
99- """
100- Store the received questions data into the following data format:
101- list( list( question_title, question_link, question_id ) )
187+ This Function creates QuestionsPanel_stackoverflow class which supports
188+ Rendering, navigation, searching and redirecting capabilities
102189 """
103- questions_data = [[item ['title' ], item ['link' ], item ['question_id' ]] for item in json_ques_data ["items" ] ]
104- # Clear terminal
105- os .system ('cls' if os .name == 'nt' else 'clear' )
106-
107- # cache array to store the requested answers. Format of storage { question_id:[answer_body, answer_link] }
108- downloaded_questions_cache = defaultdict (lambda : False )
109-
110- # Stores the currently showing question index in questions_data
111- question_posx = 0
112-
113- while True :
114- os .system ('cls' if os .name == 'nt' else 'clear' )
115- # Printing all the questions. The active question is printed GREEN.
116- console .rule ('[bold blue] Relevant Questions' , style = "bold red" )
117- for idx , question_data in enumerate (questions_data ):
118- if question_posx == idx :
119- console .print ("[green]{}. {} | {}" .format (idx + 1 , question_data [0 ], question_data [1 ]))
120- else :
121- console .print ("{}. {} | {}" .format (idx + 1 , question_data [0 ], question_data [1 ]))
122- console .rule ("[bold blue] Answer of question {}" .format (question_posx + 1 ), style = "bold red" )
123-
124- # Gets the question_id of active question
125- current_question_id = questions_data [question_posx ][2 ]
126-
127- # Searches for the id in cache. If present then prints it
128- if (downloaded_questions_cache [current_question_id ]):
129- output_content = downloaded_questions_cache [current_question_id ]
130- for output_index , output_text in enumerate (output_content ):
131- """
132- Loop through the output_text and print the element
133- if it is the last one, the text[0] is printed
134- along with text[-1]
135-
136- if text is markdown , render the markdown
137- """
138- if output_index == len (output_content ) - 1 :
139- console .print ("Link to the answer: " + output_text )
140- break
141-
142- if output_index == len (output_content ) - 2 :
143- MarkdownRenderer (output_text )
144- continue
145-
146- console .print (output_text )
147- # If the cache has no entry for the said question id, then downloads the answer
148- # and makes an for it entry in the cache array in the said format and restarts the loop.
149- else :
150- try :
151- resp = requests .get (
152- f"{ self .search_content_url } /2.2/questions/{ current_question_id } /answers?order=desc&sort=votes&site=stackoverflow&filter=!--1nZwsgqvRX"
153- )
154- except :
155- SearchError ("Search Failed" , "Try connecting to the internet" )
156- sys .exit ()
157- json_ans_data = resp .json ()
158- print (json_ans_data ["items" ])
159- most_upvoted = json_ans_data ["items" ][0 ]
160- downloaded_questions_cache [current_question_id ] = [most_upvoted ["body_markdown" ], most_upvoted ['link' ]]
161- del most_upvoted
162- continue
163-
164- console .rule ("[bold blue]" , style = "bold red" , align = "right" )
165- # Asks the user for next question number. Makes it the active question and the loop restarts
166- while True :
167- posx = int (input ("Enter the question number you want to view (Press 0 to quit): " )) - 1
168- if (posx == - 1 ):
169- return
170- elif (0 <= posx < len (questions_data )):
171- question_posx = posx
172- break
173- else :
174- console .print ("Please enter a valid question number" )
175- continue
190+ stackoverflow_panel = QuestionsPanelStackoverflow ()
191+ stackoverflow_panel .display_panel (questions_list )
192+ # Support for reddit searching can also be implemented from here
176193
177194 # Get an access token and extract to a JSON file "access_token.json"
178195 @classmethod
0 commit comments