',
59 | help='Get Problem Description.')
60 | parser.add_argument('--submit', nargs=3, required=False,
61 | metavar=('', '', ''),
62 | help='Eg: C++, C, Python, Python3, java, etc. (case-insensitive)')
63 | parser.add_argument('--search', required=False, metavar='', choices=SEARCH_TYPES,
64 | help='Search practice problems filter (case-insensitive)')
65 |
66 | # contests and its filters
67 | parser.add_argument('--contests', required=False, action='store_true',
68 | help='Get All Contests')
69 | parser.add_argument('--contest', required=False, metavar='',
70 | help='Get Contest Problems')
71 | parser.add_argument('--show-past', required=False, action='store_true',
72 | help='Shows only past contests.')
73 |
74 | # solutions & its filters
75 | parser.add_argument('--solutions', required=False, metavar='',
76 | help='Get problem\'s solutions list')
77 | parser.add_argument('--solution', required=False, metavar='',
78 | help='Get specific solution')
79 | parser.add_argument('--language', required=False,
80 | help='Language filter. Eg: C++, C, python3, java. (case-insensitive)')
81 | parser.add_argument('--result', '-r', required=False, choices=RESULT_CODES.keys(),
82 | help='Result type filter (case-insensitive)')
83 |
84 | # tags
85 | parser.add_argument('--tags', required=False, nargs='*', metavar="",
86 | help='No args: get all tags. Add args to get tagged problems')
87 |
88 | # common
89 | parser.add_argument('--lines', required=False, metavar='', default=DEFAULT_NUM_LINES,
90 | type=int, help=f'Limit number of lines. Default: {DEFAULT_NUM_LINES}')
91 | parser.add_argument('--sort', required=False, metavar='',
92 | help='utility argument to sort the results')
93 | parser.add_argument('--order', required=False, metavar='', default='asc',
94 | help='utility argument to specify the sorting order; default: `asc` \
95 | `asc` for ascending; `desc` for descending')
96 | parser.add_argument('--page', '-p', required=False, metavar='', default=DEFAULT_PAGE,
97 | type=int, help=f'Gets specific page. Default: {DEFAULT_PAGE}')
98 |
99 | return parser
100 |
101 |
102 | def main(argv=None):
103 | if argv is None:
104 | argv = sys.argv
105 |
106 | try:
107 | parser = create_parser()
108 | args = parser.parse_args(argv[1:])
109 |
110 | username = args.login
111 | is_logout = args.logout
112 | disconnect_sessions = args.disconnect_sessions
113 |
114 | user = args.user
115 | team = args.team
116 |
117 | ratings = args.ratings
118 | country = args.country
119 | institution = args.institution
120 | institution_type = args.institution_type
121 |
122 | problem_code = args.problem
123 | submit = args.submit
124 | search = args.search
125 |
126 | contest = args.contest
127 | contests = args.contests
128 | show_past = args.show_past
129 |
130 | tags = args.tags
131 |
132 | solutions = args.solutions
133 | solution_code = args.solution
134 | language = args.language
135 | result = args.result
136 |
137 | lines = args.lines
138 | sort = args.sort
139 | order = args.order
140 | page = args.page
141 |
142 | resps = []
143 |
144 | # function stylize to style the output
145 | def stylize(resps) :
146 | theme='data'
147 | if resps.get('code: ')== 404 :
148 | theme='error'
149 |
150 | remain=[]
151 | print()
152 | for ele in resps.keys() :
153 | if len(ele) > 30 or len(str(resps[ele])) > 30 :
154 | remain.append(ele)
155 | else :
156 | c.print(ele.upper(),end="",style=theme)
157 | c.print(resps[ele],end="")
158 | print(" "*10,end="")
159 | print()
160 | print()
161 | for ele in remain :
162 | c.print(ele.upper(),end="",style=theme)
163 | result=resps[ele]
164 | if ele=="Description: ":
165 | result = BeautifulSoup(result,"html.parser")
166 | im_urls = result.find_all('img')
167 | c.print(result.text)
168 | for url in im_urls :
169 | c.print("image: "+url['src'])
170 | continue
171 | c.print(result)
172 | print()
173 | return
174 |
175 | if username != INVALID_USERNAME:
176 | resps = login(username=username, disconnect_sessions=disconnect_sessions)
177 |
178 | elif is_logout:
179 | resps = logout()
180 |
181 | if problem_code:
182 | resps = get_description(problem_code, contest or CC_PRACTICE)
183 | stylize(resps)
184 | return
185 |
186 | elif submit:
187 | resps = submit_problem(*submit)
188 |
189 | elif search:
190 | resps = search_problems(sort, order, search)
191 |
192 | elif contest:
193 | resps = get_contest_problems(sort, order, contest)
194 |
195 | elif contests:
196 | resps = get_contests(show_past)
197 |
198 | elif isinstance(tags, list):
199 | resps = get_tags(sort, order, tags)
200 |
201 | elif solutions:
202 | resps = get_solutions(sort, order, solutions, page, language, result, user)
203 |
204 | elif solution_code:
205 | resps = get_solution(solution_code)
206 |
207 | elif user:
208 | resps = get_user(user)
209 |
210 | elif team:
211 | resps = get_team(team)
212 |
213 | elif ratings:
214 | resps = get_ratings(sort, order, country, institution, institution_type, page, lines)
215 |
216 | else:
217 | parser.print_help()
218 |
219 | if not resps:
220 | resps = [GENERIC_RESP]
221 |
222 | for resp in resps:
223 | print_response(**resp)
224 |
225 | return resps
226 | except KeyboardInterrupt:
227 | print('\nBye.')
228 | return [{"data": "\nBye."}]
229 | return [{"data": "0"}]
230 |
231 |
232 | if __name__ == '__main__':
233 | main(sys.argv)
234 |
--------------------------------------------------------------------------------
/codechefcli/auth.py:
--------------------------------------------------------------------------------
1 | import os
2 | from getpass import getpass
3 |
4 | from requests_html import HTMLSession
5 |
6 | from codechefcli.decorators import login_required
7 | from codechefcli.helpers import (COOKIES_FILE_PATH, CSRF_TOKEN_INPUT_ID, get_csrf_token,
8 | init_session_cookie, request, set_session_cookies)
9 |
10 | CSRF_TOKEN_MISSING = 'No CSRF Token found'
11 | SESSION_LIMIT_FORM_ID = '#session-limit-page'
12 | LOGIN_FORM_ID = 'ajax_login_form'
13 | LOGOUT_BUTTON_CLASS = '.logout-link'
14 | EMPTY_AUTH_DATA_MSG = 'Username/Password field cannot be left blank.'
15 | SESSION_LIMIT_MSG = 'Session limit exceeded!'
16 | INCORRECT_CREDS_MSG = 'Incorrect Credentials!'
17 | LOGIN_SUCCESS_MSG = 'Successfully logged in!'
18 | LOGOUT_SUCCESS_MSG = 'Successfully logged out!'
19 | LOGIN_URL = "https://www.codechef.com/api/codechef/login"
20 |
21 |
22 | def is_logged_in(resp):
23 | return not bool(resp.html.find(LOGIN_FORM_ID))
24 |
25 |
26 | def get_form_url(rhtml):
27 | form = rhtml.find(SESSION_LIMIT_FORM_ID, first=True)
28 | return form and form.element.action
29 |
30 |
31 | def get_other_active_sessions(rhtml):
32 | form = rhtml.find(SESSION_LIMIT_FORM_ID, first=True)
33 | inputs = form.find('input')
34 | inputs = inputs[:-5] + inputs[-4:]
35 | return {inp.element.name: dict(inp.element.items()).get('value', '') for inp in inputs}
36 |
37 |
38 | def disconnect_active_sessions(session, login_resp_html):
39 | token = get_csrf_token(login_resp_html, CSRF_TOKEN_INPUT_ID)
40 | post_url = get_form_url(login_resp_html)
41 | other_active_sessions = get_other_active_sessions(login_resp_html)
42 |
43 | resp = request(
44 | session=session, method='POST', url=post_url, data=other_active_sessions, token=token)
45 | if resp and hasattr(resp, 'status_code') and resp.status_code == 200:
46 | return [{'data': LOGIN_SUCCESS_MSG}]
47 | return [{'code': 503}]
48 |
49 |
50 | def save_session_cookies(session, username):
51 | session.cookies.set_cookie(init_session_cookie("username", username))
52 | session.cookies.save(ignore_expires=True, ignore_discard=True)
53 |
54 |
55 | def make_login_req(username, password, disconnect_sessions):
56 | with HTMLSession() as session:
57 | set_session_cookies(session)
58 |
59 | data = {
60 | 'name': username,
61 | 'pass': password,
62 | 'form_id': LOGIN_FORM_ID,
63 | }
64 |
65 | resp = request(url=LOGIN_URL, session=session, method='POST', data=data)
66 | resp_json = resp.json()
67 |
68 | if resp.status_code == 200:
69 | if resp_json.get('status') == "success":
70 | save_session_cookies(session, username)
71 | return [{'data': LOGIN_SUCCESS_MSG}]
72 | return [{'data': INCORRECT_CREDS_MSG, 'code': 400}]
73 | return [{'code': 503}]
74 |
75 |
76 | def login(username=None, password=None, disconnect_sessions=False):
77 | if username is None:
78 | username = input('Username: ')
79 | if password is None:
80 | password = getpass()
81 |
82 | if username and password:
83 | return make_login_req(username, password, disconnect_sessions)
84 | return [{'data': EMPTY_AUTH_DATA_MSG, 'code': 400}]
85 |
86 |
87 | @login_required
88 | def logout(session=None):
89 | resp = request(session=session, url='/logout')
90 | if resp.status_code == 200:
91 | if os.path.exists(COOKIES_FILE_PATH):
92 | os.remove(COOKIES_FILE_PATH)
93 | return [{'data': LOGOUT_SUCCESS_MSG}]
94 | return [{'code': 503}]
95 |
--------------------------------------------------------------------------------
/codechefcli/decorators.py:
--------------------------------------------------------------------------------
1 | import os
2 | from functools import wraps
3 | from http.cookiejar import LWPCookieJar
4 |
5 | from codechefcli.helpers import COOKIES_FILE_PATH
6 |
7 |
8 | def login_required(func):
9 | @wraps(func)
10 | def wrapper(*args, **kwargs):
11 | is_logged_in = False
12 | if os.path.exists(COOKIES_FILE_PATH):
13 | cookiejar = LWPCookieJar(filename=COOKIES_FILE_PATH)
14 | cookiejar.load()
15 |
16 | if len(cookiejar):
17 | is_logged_in = True
18 | else:
19 | os.remove(COOKIES_FILE_PATH)
20 | if is_logged_in is False:
21 | return [{'code': 401}]
22 | return func(*args, **kwargs)
23 | return wrapper
24 |
25 |
26 | def sort_it(func):
27 | def wrapper(*args, **kwargs):
28 | sort = args[0] and args[0].upper()
29 | order_type = args[1]
30 |
31 | resps = func(*args, **kwargs)
32 | for resp in resps:
33 | if resp.get('code', 200) == 200 and resp.get('data_type') == 'table':
34 | if sort is not None:
35 | all_rows = resp['data']
36 | heading = all_rows[0]
37 | data_rows = all_rows[1:]
38 | if not data_rows:
39 | continue
40 | if sort in heading:
41 | index = heading.index(sort)
42 |
43 | if order_type in ['asc', 'desc']:
44 | reverse = False
45 |
46 | if order_type == 'desc':
47 | reverse = True
48 |
49 | if data_rows[0][index].isdigit():
50 | for data_row in data_rows:
51 | if data_row[index].isdigit():
52 | data_row[index] = int(data_row[index])
53 | else:
54 | data_row[index] = 0
55 |
56 | data_rows.sort(key=lambda x: x[index], reverse=reverse)
57 |
58 | for data_row in data_rows:
59 | data_row[index] = str(data_row[index])
60 | else:
61 | data_rows.sort(key=lambda x: x[index], reverse=reverse)
62 |
63 | data_rows.insert(0, heading)
64 | resp['data'] = data_rows
65 | else:
66 | return [{
67 | 'code': 404,
68 | 'data': 'Wrong order argument entered.',
69 | 'data_type': 'text'
70 | }]
71 | else:
72 | return [{
73 | 'code': 404,
74 | 'data': 'Wrong sorting argument entered.',
75 | 'data_type': 'text'
76 | }]
77 | return resps
78 | return wraps(func)(wrapper)
79 |
--------------------------------------------------------------------------------
/codechefcli/helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from http.cookiejar import Cookie, LWPCookieJar
4 | from os.path import expanduser
5 | from pydoc import pager
6 |
7 | from requests import ReadTimeout
8 | from requests.exceptions import ConnectionError
9 | from requests_html import HTMLSession
10 |
11 | CSRF_TOKEN_INPUT_ID = 'edit-csrfToken'
12 | MIN_NUM_SPACES = 3
13 | BASE_URL = 'https://www.codechef.com'
14 | SERVER_DOWN_MSG = 'Please try again later. Seems like CodeChef server is down!'
15 | INTERNET_DOWN_MSG = 'Nothing to show. Check your internet connection.'
16 | UNAUTHORIZED_MSG = 'You are not logged in.'
17 | COOKIES_FILE_PATH = expanduser('~') + '/.cookies'
18 | BCOLORS = {
19 | 'HEADER': '\033[95m',
20 | 'BLUE': '\033[94m',
21 | 'GREEN': '\033[92m',
22 | 'WARNING': '\033[93m',
23 | 'FAIL': '\033[91m',
24 | 'ENDC': '\033[0m',
25 | 'BOLD': '\033[1m',
26 | 'UNDERLINE': '\033[4m'
27 | }
28 |
29 |
30 | def set_session_cookies(session):
31 | session.cookies = LWPCookieJar(filename=COOKIES_FILE_PATH)
32 |
33 |
34 | def get_session():
35 | session = HTMLSession()
36 |
37 | if os.path.exists(COOKIES_FILE_PATH):
38 | set_session_cookies(session)
39 | session.cookies.load(ignore_discard=True, ignore_expires=True)
40 | return session
41 |
42 |
43 | def init_session_cookie(name, value, **kwargs):
44 | return Cookie(version=0, name=name, value=value, port=None, port_specified=False,
45 | domain='www.codechef.com', domain_specified=False, domain_initial_dot=False,
46 | path='/', path_specified=True, secure=False, expires=None, discard=False,
47 | comment=None, comment_url=None, rest={'HttpOnly': None}, rfc2109=False)
48 |
49 |
50 | def get_username():
51 | session = get_session()
52 |
53 | for index, cookie in enumerate(session.cookies):
54 | if cookie.name == 'username':
55 | return cookie.value
56 |
57 | return None
58 |
59 |
60 | def request(session=None, method="GET", url="", token=None, **kwargs):
61 | if not session:
62 | session = get_session()
63 | if token:
64 | session.headers = getattr(session, 'headers') or {}
65 | session.headers.update({'X-CSRF-Token': token})
66 |
67 | if BASE_URL not in url:
68 | url = f'{BASE_URL}{url}'
69 |
70 | try:
71 | return session.request(method=method, url=url, timeout=(15, 15), **kwargs)
72 | except (ConnectionError, ReadTimeout):
73 | print(INTERNET_DOWN_MSG)
74 | sys.exit(1)
75 |
76 |
77 | def html_to_list(table):
78 | if not table:
79 | return []
80 |
81 | rows = table.find('tr')
82 | data_rows = [[header.text.strip().upper() for header in rows[0].find('th, td')]]
83 | for row in rows[1:]:
84 | data_rows.append([col.text.strip() for col in row.find('td')])
85 | return data_rows
86 |
87 |
88 | def get_col_max_lengths(data_rows, num_cols):
89 | max_len_in_cols = [0] * num_cols
90 | for row in data_rows:
91 | for index, val in enumerate(row):
92 | if len(val) > max_len_in_cols[index]:
93 | max_len_in_cols[index] = len(val)
94 | return max_len_in_cols
95 |
96 |
97 | def print_table(data_rows, min_num_spaces=MIN_NUM_SPACES, is_pager=True):
98 | if len(data_rows) == 0:
99 | return
100 |
101 | max_len_in_cols = get_col_max_lengths(data_rows, len(data_rows[0]))
102 |
103 | table = []
104 | for row in data_rows:
105 | _row = []
106 | for index, val in enumerate(row):
107 | num_spaces = max_len_in_cols[index] - len(val) + min_num_spaces
108 | _row.append(val + (num_spaces * ' '))
109 | table.append("".join(_row))
110 |
111 | table_str = '\n\n'.join(table)
112 | if is_pager:
113 | pager(table_str)
114 | print(table_str)
115 | return table_str
116 |
117 |
118 | def style_text(text, color=None):
119 | if color is None or BCOLORS.get(color) is None:
120 | return text
121 |
122 | return '{0}{1}{2}'.format(BCOLORS[color], text, BCOLORS['ENDC'])
123 |
124 |
125 | def print_response_util(data, extra, data_type, color, is_pager=True):
126 | if data is None and extra is None:
127 | no_data_msg = style_text('Nothing to show.', 'WARNING')
128 | print(no_data_msg)
129 | return no_data_msg, None
130 |
131 | return_val = None
132 | if data is not None:
133 | if data_type == 'table':
134 | return_val = print_table(data, is_pager=is_pager)
135 | elif data_type == 'text':
136 | if is_pager:
137 | pager(style_text(data, color))
138 | return_val = style_text(data, color)
139 | print(return_val)
140 |
141 | styled_extra = None
142 | if extra is not None:
143 | styled_extra = style_text(extra, color)
144 | print(styled_extra)
145 | return return_val, styled_extra
146 |
147 |
148 | def print_response(data_type='text', code=200, data=None, extra=None, **kwargs):
149 | color = None
150 |
151 | if code == 503:
152 | if not data:
153 | data = SERVER_DOWN_MSG
154 | color = 'FAIL'
155 | elif code == 404 or code == 400:
156 | color = 'WARNING'
157 | elif code == 401:
158 | if not data:
159 | data = UNAUTHORIZED_MSG
160 | color = 'FAIL'
161 |
162 | is_pager = False
163 | if not hasattr(kwargs, 'is_pager') and data_type == 'table':
164 | is_pager = True
165 | else:
166 | is_pager = kwargs.get('is_pager', False)
167 |
168 | return print_response_util(data, extra, data_type, color, is_pager=is_pager)
169 |
170 |
171 | def get_csrf_token(rhtml, selector):
172 | token = rhtml.find(f"#{selector}", first=True)
173 | return token and hasattr(token.element, 'value') and token.element.value
174 |
--------------------------------------------------------------------------------
/codechefcli/problems.py:
--------------------------------------------------------------------------------
1 | import math
2 | import re
3 |
4 | from requests_html import HTML
5 |
6 | from codechefcli.auth import is_logged_in
7 | from codechefcli.decorators import login_required, sort_it
8 | from codechefcli.helpers import (BASE_URL, CSRF_TOKEN_INPUT_ID, SERVER_DOWN_MSG, get_csrf_token,
9 | html_to_list, request, style_text)
10 |
11 | LANGUAGE_SELECTOR = "#language"
12 | INVALID_PROBLEM_CODE_MSG = 'Invalid Problem Code.'
13 | PAGE_INFO_CLASS = '.pageinfo'
14 | PROBLEM_SUBMISSION_FORM_ID = '#problem-submission'
15 | PROBLEM_SUB_DATA_FORM_ID = 'problem_submission'
16 | PROBLEM_SUBMISSION_INPUT_ID = '#edit-problem-submission-form-token'
17 | LANGUAGE_DROPDOWN_ID = '#edit-language'
18 | COMPILATION_ERROR_CLASS = '.cc-error-txt'
19 | PROBLEM_LIST_TABLE_HEADINGS = ['CODE', 'NAME', 'SUBMISSION', 'ACCURACY']
20 | RESULT_CODES = {'AC': 15, 'WA': 14, 'TLE': 13, 'RTE': 12, 'CTE': 11}
21 | RATINGS_TABLE_HEADINGS = ['GLOBAL(COUNTRY)', 'USER NAME', 'RATING', 'GAIN/LOSS']
22 | SOLUTION_ERR_MSG_CLASS = '.err-message'
23 | INVALID_SOLUTION_ID_MSG = "Invalid solution ID"
24 |
25 |
26 | def get_description(problem_code, contest_code):
27 | url = f'/api/contests/{contest_code}/problems/{problem_code}'
28 | resp = request(url=url)
29 |
30 | try:
31 | resp_json = resp.json()
32 | except ValueError:
33 | return [{'code': 503}]
34 |
35 | if resp_json["status"] == "success":
36 | problem = {
37 | 'Name: ': resp_json.get('problem_name', ''),
38 | "Author: " : resp_json.get('problem_author', ''),
39 | "Date Added: " : resp_json.get('date_added', ''),
40 | "Max Time Limit: ": f"{resp_json.get('max_timelimit', '')} secs",
41 | "Source Limit: " : f"{resp_json.get('source_sizelimit', '')} Bytes",
42 | "Languages: " : resp_json.get('languages_supported', ''),
43 | "Description: ": resp_json.get("body", ''), #re.sub(r'(<|<\/)\w+>', '',
44 | }
45 | if resp_json.get('tags'):
46 | problem['Tags: ']= " ".join([tag.text for tag in HTML(html=resp_json['tags']).find('a')])
47 |
48 | if resp_json.get('editorial_url'):
49 | problem['Editorial: '] = resp_json['editorial_url']
50 |
51 | return problem
52 | elif resp_json["status"] == "error":
53 | problem= {
54 | 'data: ': 'Problem not found. Use `--search` to search in a specific contest',
55 | 'code: ': 404
56 | }
57 | return problem
58 | return [{'code': 503}]
59 |
60 |
61 | def get_form_token(rhtml):
62 | form = rhtml.find(PROBLEM_SUBMISSION_FORM_ID, first=True)
63 | inp = form and form.find(PROBLEM_SUBMISSION_INPUT_ID, first=True)
64 | input_element = inp and hasattr(inp, 'element') and inp.element
65 | return input_element is not None and hasattr(input_element, 'value') and input_element.value
66 |
67 |
68 | def get_status_table(status_code):
69 | resp = request(url=f'/error_status_table/{status_code}')
70 | if resp.status_code != 200 or not resp.text:
71 | return
72 | return resp.html
73 |
74 |
75 | def get_compilation_error(status_code):
76 | resp = request(url=f'/view/error/{status_code}')
77 | if resp.status_code == 200:
78 | return resp.html.find(COMPILATION_ERROR_CLASS, first=True).text
79 | return SERVER_DOWN_MSG
80 |
81 |
82 | def get_language_code(rhtml, language):
83 | form = rhtml.find(PROBLEM_SUBMISSION_FORM_ID, first=True)
84 | languages_dropdown = form.find(LANGUAGE_DROPDOWN_ID, first=True)
85 | for option in languages_dropdown.find('option'):
86 | if language.lower() + '(' in option.text.lower():
87 | return dict(option.element.items())['value']
88 |
89 |
90 | @login_required
91 | def submit_problem(problem_code, solution_file, language):
92 | url = f'/submit/{problem_code}'
93 | get_resp = request(url=url)
94 |
95 | if not is_logged_in(get_resp):
96 | return [{"code": 401, "data": "This session has been disconnected. Login again."}]
97 |
98 | if get_resp.status_code == 200:
99 | rhtml = get_resp.html
100 | form_token = get_form_token(rhtml)
101 | language_code = get_language_code(rhtml, language)
102 | csrf_token = get_csrf_token(rhtml, CSRF_TOKEN_INPUT_ID)
103 |
104 | if language_code is None:
105 | return [{'code': 400, 'data': 'Invalid language.'}]
106 | else:
107 | return [{'code': 503}]
108 |
109 | try:
110 | solution_file_obj = open(solution_file)
111 | except IOError:
112 | return [{'data': 'Solution file not found.', 'code': 400}]
113 |
114 | data = {
115 | 'language': language_code,
116 | 'problem_code': problem_code,
117 | 'form_id': PROBLEM_SUB_DATA_FORM_ID,
118 | 'form_token': form_token
119 | }
120 | files = {'files[sourcefile]': solution_file_obj}
121 |
122 | post_resp = request(method='POST', url=url, data=data, files=files)
123 | if post_resp.status_code == 200:
124 | print(style_text('Submitting code...\n', 'BLUE'))
125 |
126 | status_code = post_resp.url.split('/')[-1]
127 | url = f'/get_submission_status/{status_code}'
128 | print(style_text('Fetching results...\n', 'BLUE'))
129 |
130 | max_tries = 3
131 | num_tries = 0
132 | while True:
133 | resp = request(url=url, token=csrf_token)
134 | num_tries += 1
135 |
136 | try:
137 | status_json = resp.json()
138 | except ValueError:
139 | if num_tries == max_tries:
140 | return [{'code': 503}]
141 | continue
142 |
143 | result_code = status_json['result_code']
144 |
145 | if result_code != 'wait':
146 | data = ''
147 | if result_code == 'compile':
148 | error_msg = get_compilation_error(status_code)
149 | data = style_text(f'Compilation error.\n{error_msg}', 'FAIL')
150 | elif result_code == 'runtime':
151 | data = style_text(f"Runtime error. {status_json.get('signal', '')}\n", 'FAIL')
152 | elif result_code == 'wrong':
153 | data = style_text('Wrong answer\n', 'FAIL')
154 | elif result_code == 'accepted':
155 | data = 'Correct answer\n'
156 |
157 | resps = [{'data': data}]
158 | status_table = get_status_table(status_code)
159 | if status_table:
160 | resps.append({'data_type': 'table', 'data': html_to_list(status_table)})
161 | return resps
162 | else:
163 | print(style_text('Waiting...\n', 'BLUE'))
164 | return [{'code': 503}]
165 |
166 |
167 | @sort_it
168 | def get_contest_problems(sort, order, contest_code):
169 | url = f'/api/contests/{contest_code}?'
170 | resp = request(url=url)
171 |
172 | try:
173 | resp_json = resp.json()
174 | except ValueError:
175 | return [{"code": 503}]
176 |
177 | if resp_json['status'] == "success":
178 | problems_table = [[
179 | x.upper() for x in [
180 | "Name", "Code", "URL", "Successful Submissions", "Accuracy", "Scorable?"]
181 | ]]
182 | for _, problem in resp_json['problems'].items():
183 | problems_table.append([
184 | problem['name'],
185 | problem['code'],
186 | f"{BASE_URL}{problem['problem_url']}",
187 | problem['successful_submissions'],
188 | f"{problem['accuracy']} %",
189 | "Yes" if problem['category_name'] == 'main' else "No"
190 | ])
191 |
192 | return [
193 | {'data': f"\n{style_text('Name:', 'BOLD')} {resp_json['name']}\n"},
194 | {'data': problems_table, "data_type": "table"},
195 | {'data': f'\n{style_text("Announcements", "BOLD")}:\n{resp_json["announcements"]}'}
196 | ]
197 | elif resp_json['status'] == "error":
198 | return [{'data': 'Contest doesn\'t exist.', 'code': 404}]
199 | return [{"code": 503}]
200 |
201 |
202 | @sort_it
203 | def search_problems(sort, order, search_type):
204 | url = f'/problems/{search_type.lower()}'
205 | resp = request(url=url)
206 | if resp.status_code == 200:
207 | return [{'data_type': 'table', 'data': html_to_list(resp.html.find('table')[1])}]
208 | return [{"code": 503}]
209 |
210 |
211 | def get_tags(sort, order, tags):
212 | if len(tags) == 0:
213 | return get_all_tags()
214 | return get_tagged_problems(sort, order, tags)
215 |
216 |
217 | def get_all_tags():
218 | resp = request(url='/get/tags/problems')
219 |
220 | try:
221 | all_tags = resp.json()
222 | except ValueError:
223 | return [{'code': 503}]
224 |
225 | if resp.status_code == 200:
226 | data_rows = []
227 | num_cols = 5
228 | row = []
229 |
230 | for index, tag in enumerate(all_tags):
231 | tag_name = tag.get('tag', '')
232 | if len(row) < num_cols:
233 | row.append(tag_name)
234 | else:
235 | data_rows.append(row)
236 | row = [tag_name]
237 | if len(row):
238 | data_rows.append(row)
239 |
240 | return [{'data': data_rows, 'data_type': 'table'}]
241 |
242 | return [{'code': 503}]
243 |
244 |
245 | @sort_it
246 | def get_tagged_problems(sort, order, tags):
247 | resp = request(url=f'/get/tags/problems/{",".join(tags)}')
248 |
249 | try:
250 | all_tags = resp.json()
251 | except ValueError:
252 | return [{'code': 503}]
253 |
254 | if resp.status_code == 200:
255 | data_rows = [PROBLEM_LIST_TABLE_HEADINGS]
256 | all_tags = all_tags.get('all_problems')
257 |
258 | if not all_tags:
259 | return [{'code': 404, 'extra': "Sorry, there are no problems with the following tags!"}]
260 |
261 | for _, problem in all_tags.items():
262 | problem_info = [
263 | problem.get('code', ''),
264 | problem.get('name', ''),
265 | str(problem.get('attempted_by', ''))
266 | ]
267 | try:
268 | accuracy = (problem.get('solved_by') / problem.get('attempted_by')) * 100
269 | problem_info.append(str(math.floor(accuracy)))
270 | except TypeError:
271 | problem_info.append('')
272 | data_rows.append(problem_info)
273 |
274 | return [{'data': data_rows, 'data_type': 'table'}]
275 |
276 | return [{'code': 503}]
277 |
278 |
279 | @sort_it
280 | def get_ratings(sort, order, country, institution, institution_type, page, lines):
281 | csrf_resp = request(url='/ratings/all')
282 | if csrf_resp.status_code == 200:
283 | csrf_token = get_csrf_token(csrf_resp.html, CSRF_TOKEN_INPUT_ID)
284 | else:
285 | return [{'code': 503}]
286 |
287 | url = '/api/ratings/all?sortBy=global_rank&order=asc'
288 | params = {'page': str(page), 'itemsPerPage': str(lines), 'filterBy': ''}
289 | if country:
290 | params['filterBy'] += f'Country={country};'
291 | if institution:
292 | institution = institution.title()
293 | params['filterBy'] += f'Institution={institution};'
294 | if institution_type:
295 | params['filterBy'] += f'Institution type={institution_type};'
296 |
297 | resp = request(url=url, params=params, token=csrf_token)
298 |
299 | if resp.status_code == 200:
300 | try:
301 | ratings = resp.json()
302 | except ValueError:
303 | return [{'code': 503}]
304 |
305 | ratings = ratings.get('list') or []
306 | if len(ratings) == 0:
307 | return [{'code': 404, 'data': 'No ratings found'}]
308 |
309 | data_rows = [RATINGS_TABLE_HEADINGS]
310 | for user in ratings:
311 | data_rows.append([
312 | f"{str(user['global_rank'])} ({str(user['country_rank'])})",
313 | user['username'],
314 | str(user['rating']),
315 | str(user['diff'])
316 | ])
317 | return [{'data': data_rows, 'data_type': 'table'}]
318 | return [{'code': 503}]
319 |
320 |
321 | def get_contests(show_past):
322 | resp = request(url='/contests')
323 | if resp.status_code == 200:
324 | tables = resp.html.find('table')
325 | labels = ['Present', 'Future']
326 | if show_past:
327 | labels = ['Past']
328 | tables = [tables[0], tables[-1]]
329 |
330 | resps = []
331 | for idx, label in enumerate(labels):
332 | resps += [
333 | {'data': style_text(f'{label} Contests:\n', 'BOLD')},
334 | {'data': html_to_list(tables[idx + 1]), 'data_type': 'table'}
335 | ]
336 | return resps
337 | return [{'code': 503}]
338 |
339 |
340 | def build_request_params(resp_html, language, result, username, page):
341 | params = {'page': page - 1} if page != 1 else {}
342 | if language:
343 | lang_dropdown = resp_html.find(LANGUAGE_SELECTOR, first=True)
344 | options = lang_dropdown.find('option')
345 |
346 | for option in options:
347 | if language.upper() == option.text.strip().upper():
348 | params['language'] = dict(option.element.items()).get('value', '')
349 | break
350 | if result:
351 | params['status'] = RESULT_CODES[result.upper()]
352 | if username:
353 | params['handle'] = username
354 | return params
355 |
356 |
357 | @sort_it
358 | def get_solutions(sort, order, problem_code, page, language, result, username):
359 | url = f'/status/{problem_code.upper()}'
360 | resp = request(url=url)
361 |
362 | if resp.status_code != 200:
363 | return [{'code': 503}]
364 |
365 | params = build_request_params(resp.html, language, result, username, page)
366 | resp = request(url=url, params=params)
367 |
368 | if resp.status_code == 200:
369 | if problem_code in resp.url:
370 | resp_html = resp.html
371 | solution_table = resp_html.find('table')[2]
372 | page_info = resp_html.find(PAGE_INFO_CLASS, first=True)
373 |
374 | data_rows = html_to_list(solution_table)
375 | for row in data_rows:
376 | # remove view solution column
377 | del row[-1]
378 |
379 | # format result column
380 | row[3] = ' '.join(row[3].split('\n'))
381 |
382 | resp = {'data_type': 'table', 'data': data_rows}
383 | if page_info:
384 | resp['extra'] = f'\nPage: {page_info.text}'
385 |
386 | return [resp]
387 | else:
388 | return [{'code': 404, 'data': INVALID_PROBLEM_CODE_MSG}]
389 | return [{'code': 503}]
390 |
391 |
392 | def get_solution(solution_code):
393 | resp = request(url=f'/viewplaintext/{solution_code}')
394 | if resp.status_code == 200:
395 | err_msg_element = resp.html.find(SOLUTION_ERR_MSG_CLASS, first=True)
396 | if err_msg_element and err_msg_element.text == INVALID_SOLUTION_ID_MSG:
397 | return [{'code': 404, "data": "Invalid Solution ID"}]
398 | return [{'data': f'\n{resp.html.find("pre", first=True).element.text}\n'}]
399 | return [{'code': 503}]
400 |
--------------------------------------------------------------------------------
/codechefcli/teams.py:
--------------------------------------------------------------------------------
1 | from codechefcli.helpers import BASE_URL, html_to_list, request
2 |
3 |
4 | def get_team_url(name):
5 | return f"{BASE_URL}/teams/view/{name}"
6 |
7 |
8 | def format_contest(item):
9 | if item.startswith("Information for"):
10 | return f"\n{item}"
11 | return item
12 |
13 |
14 | def get_team(name):
15 | if not name:
16 | return []
17 | resp = request(url=get_team_url(name))
18 |
19 | if resp.status_code == 200:
20 | resp_html = resp.html
21 | tables = resp_html.find('table')
22 |
23 | header = tables[1].text.strip()
24 | team_info = tables[2].text.strip()
25 | team_info = team_info.replace(':\n', ': ')
26 | team_info_list = team_info.split('\n')
27 |
28 | basic_info = "\n".join(team_info_list[:2])
29 | contests_info = "\n".join([format_contest(item) for item in team_info_list[2:-1]])
30 | problems_solved_table = html_to_list(tables[-1])
31 |
32 | team_details = "\n".join([
33 | '',
34 | header,
35 | '',
36 | basic_info,
37 | contests_info,
38 | '',
39 | 'Problems Successfully Solved:',
40 | ''
41 | ])
42 | return [
43 | {'data': team_details},
44 | {'data': problems_solved_table, "data_type": "table", "is_pager": False}
45 | ]
46 | elif resp.status_code == 404:
47 | return [{'code': 404, 'data': 'Team not found.'}]
48 | return [{'code': 503}]
49 |
--------------------------------------------------------------------------------
/codechefcli/users.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from codechefcli.helpers import BASE_URL, request, style_text
4 | from codechefcli.teams import get_team_url
5 |
6 | HEADER = 'header'
7 | RATING_NUMBER_CLASS = '.rating-number'
8 | RATING_RANKS_CLASS = '.rating-ranks'
9 | STAR_RATING_CLASS = '.rating'
10 | USER_DETAILS_CONTAINER_CLASS = '.user-details-container'
11 | USER_DETAILS_CLASS = '.user-details'
12 |
13 |
14 | def get_user_teams_url(username):
15 | return f'{BASE_URL}/users/{username}/teams/'
16 |
17 |
18 | def format_list_item(item):
19 | return ": ".join([i.strip() for i in item.text.split(':')])
20 |
21 |
22 | def get_user(username):
23 | if not username:
24 | return []
25 |
26 | resp = request(url=f'/users/{username}')
27 |
28 | if resp.status_code == 200:
29 | team_url = get_team_url(username)
30 | if resp.url == team_url:
31 | return [{
32 | 'data': f'This is a team handle.'
33 | f'Run `codechefcli --team {username}` to get team info\n',
34 | 'code': 400
35 | }]
36 | elif resp.url.rstrip('/') == BASE_URL:
37 | return [{'code': 404, 'data': 'User not found.'}]
38 | else:
39 | resp_html = resp.html
40 | details_container = resp_html.find(USER_DETAILS_CONTAINER_CLASS, first=True)
41 |
42 | # basic info
43 | header = details_container.find(HEADER, first=True).text.strip()
44 | info_list_items = details_container.find(USER_DETAILS_CLASS, first=True).find('li')
45 |
46 | # ignore first & last item i.e. username item & teams item respectively
47 | info = "\n".join([format_list_item(li) for li in info_list_items[1:-1]])
48 |
49 | # rating
50 | star_rating = details_container.find(STAR_RATING_CLASS, first=True).text.strip()
51 | rating = resp_html.find(RATING_NUMBER_CLASS, first=True).text.strip()
52 | rank_items = resp_html.find(RATING_RANKS_CLASS, first=True).find('li')
53 | global_rank = rank_items[0].find('a', first=True).text.strip()
54 | country_rank = rank_items[1].find('a', first=True).text.strip()
55 |
56 | user_details = "\n".join([
57 | '',
58 | style_text(f'User Details for {header} ({username}):', 'BOLD'),
59 | '',
60 | info,
61 | f"User's Teams: {get_user_teams_url(username)}",
62 | '',
63 | f'Rating: {star_rating} {rating}',
64 | f'Global Rank: {global_rank}',
65 | f'Country Rank: {country_rank}',
66 | '',
67 | f'Find more at: {resp.url}',
68 | ''
69 | ])
70 | return [{'data': user_details}]
71 | return [{'code': 503}]
72 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flake8==3.8.3
2 | isort==4.3.21
3 | pytest==5.4.3
4 | requests_html==0.10.0
5 | coverage==5.1
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | # This flag says that the code is written to work on both Python 2 and Python
3 | # 3. If at all possible, it is good practice to do this. If you cannot, you
4 | # will need to generate wheels for each Python version that you support.
5 | universal=1
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 | from codecs import open
5 |
6 | from setuptools import setup
7 |
8 | if sys.version < '3.8':
9 | print("This version is not supported.")
10 | sys.exit(1)
11 |
12 | with open('README.rst') as f:
13 | longd = f'\n\n{f.read()}'
14 |
15 | setup(
16 | name='codechefcli',
17 | include_package_data=True,
18 | packages=["codechefcli"],
19 | data_files=[('codechefcli', [])],
20 | entry_points={"console_scripts": ['codechefcli = codechefcli.__main__:main']},
21 | install_requires=['requests_html'],
22 | python_requires='>=3.8',
23 | version='0.5.3',
24 | url='http://www.github.com/sk364/codechef-cli',
25 | keywords="codechefcli codechef cli programming competitive-programming competitive-coding",
26 | license='GNU','MIT',
27 | author='Sachin Kukreja',
28 | author_email='skad5455@gmail.com',
29 | description='CodeChef Command Line Interface',
30 | long_description=longd
31 | )
32 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sk364/codechef-cli/fa678861a51ee374029a96ffb15c43dc741f22c4/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_auth_entry.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from _pytest.monkeypatch import MonkeyPatch
4 | from requests_html import HTML
5 |
6 | from codechefcli import __main__ as entry_point
7 | from codechefcli import auth
8 | from codechefcli.auth import (CSRF_TOKEN_MISSING, EMPTY_AUTH_DATA_MSG, INCORRECT_CREDS_MSG,
9 | LOGIN_SUCCESS_MSG, LOGOUT_BUTTON_CLASS, SESSION_LIMIT_FORM_ID,
10 | SESSION_LIMIT_MSG, disconnect_active_sessions, login)
11 | from codechefcli.helpers import CSRF_TOKEN_INPUT_ID
12 | from tests.utils import MockHTMLResponse
13 |
14 |
15 | class EntryPointTests(TestCase):
16 | def setUp(self):
17 | self.monkeypatch = MonkeyPatch()
18 |
19 | def test_main_invalid_args(self):
20 | """Should raise SystemExit exception on incorrect number of args for a particular option"""
21 | with self.assertRaises(SystemExit):
22 | entry_point.main(['codechefcli', '--problem'])
23 |
24 | def test_main_valid_args(self):
25 | """Should return responses when valid args are present"""
26 | def mock_get_desc(*args, **kwargs):
27 | return [{"data": "Lots of description. Some math. Some meta info. Done."}]
28 |
29 | self.monkeypatch.setattr(entry_point, "get_description", mock_get_desc)
30 |
31 | resps = entry_point.main(['codechefcli', '--problem', 'CCC'])
32 | self.assertEqual(resps[0]["data"], "Lots of description. Some math. Some meta info. Done.")
33 |
34 | def test_create_parser(self):
35 | """Should not explode when parser is parsing the args"""
36 |
37 | parser = entry_point.create_parser()
38 | args = parser.parse_args(['--problem', 'WEICOM'])
39 | self.assertEqual(args.problem, 'WEICOM')
40 |
41 |
42 | class LoginTests(TestCase):
43 | def setUp(self):
44 | self.monkeypatch = MonkeyPatch()
45 |
46 | def test_empty_auth_data(self):
47 | """Should return empty auth data message"""
48 | resps = login(username='', password='', disconnect_sessions=False)
49 | self.assertEqual(resps[0]['data'], EMPTY_AUTH_DATA_MSG)
50 | self.assertEqual(resps[0]['code'], 400)
51 |
52 | def test_correct_auth_data(self):
53 | """Should login on correct auth data"""
54 | def mock_request(*args, **kwargs):
55 | if kwargs.get('method'):
56 | return MockHTMLResponse(
57 | data=f'')
58 | else:
59 | return MockHTMLResponse(data=f"")
60 |
61 | def mock_save_cookies(*args, **kwargs):
62 | pass
63 |
64 | self.monkeypatch.setattr(auth, 'request', mock_request)
65 | self.monkeypatch.setattr(auth, 'save_session_cookies', mock_save_cookies)
66 |
67 | resps = login(username='cc', password='cc', disconnect_sessions=False)
68 | self.assertEqual(resps[0]['data'], LOGIN_SUCCESS_MSG)
69 |
70 | def test_incorrect_auth_data(self):
71 | """Should return incorrect creds message"""
72 | def mock_request(*args, **kwargs):
73 | if kwargs.get('method'):
74 | return MockHTMLResponse(data='')
75 | else:
76 | return MockHTMLResponse(data=f"")
77 |
78 | self.monkeypatch.setattr(auth, 'request', mock_request)
79 |
80 | resps = login(username='nope', password='nope', disconnect_sessions=False)
81 | self.assertEqual(resps[0]['data'], INCORRECT_CREDS_MSG)
82 | self.assertEqual(resps[0]['code'], 400)
83 |
84 | def test_no_csrf_token(self):
85 | """Should return csrf token missing message when there isn't one in the response html"""
86 | def mock_request(*args, **kwargs):
87 | return MockHTMLResponse(data="")
88 | self.monkeypatch.setattr(auth, 'request', mock_request)
89 |
90 | resps = login(username='cc', password='cc', disconnect_sessions=False)
91 | self.assertEqual(resps[0]['data'], CSRF_TOKEN_MISSING)
92 | self.assertEqual(resps[0]['code'], 500)
93 |
94 | def test_status_code_not_200(self):
95 | """Should return code 503 when status code is not 200"""
96 | def mock_request(*args, **kwargs):
97 | if kwargs.get('method'):
98 | return MockHTMLResponse(status_code=500)
99 | else:
100 | return MockHTMLResponse(data=f"")
101 |
102 | self.monkeypatch.setattr(auth, 'request', mock_request)
103 |
104 | resps = login(username='cc', password='cc', disconnect_sessions=False)
105 | self.assertEqual(resps[0]['code'], 503)
106 |
107 | def test_session_limit_exceeded_no_disconnect(self):
108 | """Should return session limit msg on no disconnect"""
109 |
110 | def mock_request(*args, **kwargs):
111 | if kwargs.get('method'):
112 | return MockHTMLResponse(data=f'')
113 | else:
114 | return MockHTMLResponse(data=f"")
115 |
116 | def mock_logout(*args, **kwargs):
117 | pass
118 |
119 | self.monkeypatch.setattr(auth, 'request', mock_request)
120 | self.monkeypatch.setattr(auth, 'logout', mock_logout)
121 |
122 | resps = login(username='cc', password='cc', disconnect_sessions=False)
123 | self.assertEqual(resps[0]['data'], SESSION_LIMIT_MSG)
124 | self.assertEqual(resps[0]['code'], 400)
125 |
126 | def test_session_limit_exceeded_disconnect(self):
127 | """Should disconnect active sessions and login in the current returning login success msg"""
128 |
129 | def mock_request(*args, **kwargs):
130 | if kwargs.get('method'):
131 | return MockHTMLResponse(data=f'')
132 | else:
133 | return MockHTMLResponse(data=f"")
134 |
135 | def mock_logout(*args, **kwargs):
136 | pass
137 |
138 | def mock_disconnect(*args, **kwargs):
139 | return [{'data': LOGIN_SUCCESS_MSG}]
140 |
141 | def mock_save_cookies(*args, **kwargs):
142 | pass
143 |
144 | self.monkeypatch.setattr(auth, 'request', mock_request)
145 | self.monkeypatch.setattr(auth, 'logout', mock_logout)
146 | self.monkeypatch.setattr(auth, 'disconnect_active_sessions', mock_disconnect)
147 | self.monkeypatch.setattr(auth, 'save_session_cookies', mock_save_cookies)
148 |
149 | resps = login(username='cc', password='cc', disconnect_sessions=True)
150 | self.assertEqual(resps[0]['data'], LOGIN_SUCCESS_MSG)
151 |
152 | def test_disconnect_active_sessions_success(self):
153 | """Should return login success msg on disconnect"""
154 | def mock_request(*args, **kwargs):
155 | return MockHTMLResponse()
156 |
157 | self.monkeypatch.setattr(auth, 'request', mock_request)
158 |
159 | inputs = "".join([f"" for idx in range(6)])
160 | html = HTML(html=f''
161 | f'')
162 | resps = disconnect_active_sessions(None, html)
163 | self.assertEqual(resps[0]['data'], LOGIN_SUCCESS_MSG)
164 |
165 | def test_disconnect_active_sessions_error(self):
166 | """Should return 503 when status code is not 200"""
167 | def mock_request(*args, **kwargs):
168 | return MockHTMLResponse(status_code=500)
169 |
170 | self.monkeypatch.setattr(auth, 'request', mock_request)
171 |
172 | inputs = "".join([f"" for idx in range(6)])
173 | html = HTML(html=f''
174 | f'')
175 | resps = disconnect_active_sessions(None, html)
176 | self.assertEqual(resps[0]['code'], 503)
177 |
--------------------------------------------------------------------------------
/tests/test_helpers.py:
--------------------------------------------------------------------------------
1 | from http.cookiejar import Cookie
2 | from unittest import TestCase
3 |
4 | from requests_html import HTML, HTMLSession
5 |
6 | from codechefcli.helpers import (SERVER_DOWN_MSG, UNAUTHORIZED_MSG, get_csrf_token, get_session,
7 | get_username, html_to_list, init_session_cookie, print_response,
8 | print_table)
9 | from tests.utils import fake_login, fake_logout
10 |
11 |
12 | class HelpersTestCase(TestCase):
13 | def test_get_session_cookies(self):
14 | """Should return requests_html.HTMLSession instance preloaded with cookies"""
15 | fake_login()
16 |
17 | session = get_session()
18 | self.assertIsInstance(session, HTMLSession)
19 | self.assertTrue(len(session.cookies) > 0)
20 |
21 | def test_get_session_no_cookies(self):
22 | """Should return requests_html.HTMLSession instance"""
23 | fake_logout()
24 |
25 | session = get_session()
26 | self.assertIsInstance(session, HTMLSession)
27 | self.assertEqual(len(session.cookies), 0)
28 |
29 | def test_init_session_cookie(self):
30 | """Should return cookiejar.Cookie instance with name and value as provided"""
31 | cookie = init_session_cookie("u", "u")
32 | self.assertIsInstance(cookie, Cookie)
33 | self.assertEqual(cookie.name, "u")
34 | self.assertEqual(cookie.value, "u")
35 |
36 | def test_get_username_not_exists(self):
37 | """Should return None when username not found in session cookies"""
38 | fake_logout()
39 | self.assertIsNone(get_username())
40 |
41 | def test_get_username_exists(self):
42 | """Should return None when username not found in session cookies"""
43 | fake_login(init_cookies=[{"name": "username", "value": "abcd"}])
44 | self.assertEqual(get_username(), "abcd")
45 |
46 | def test_html_to_list_none_html(self):
47 | """Should return empty list when no html is provided"""
48 | self.assertTrue(len(html_to_list(None)) == 0)
49 |
50 | def test_html_to_list_valid_html(self):
51 | """Should convert requests_html.HTML instance to `list`"""
52 | html = HTML(html=" \
53 | A V \
54 | a1 v1 \
55 | a2 v2 \
56 | ")
57 | self.assertEqual(html_to_list(html), [['A', 'V'], ['a1', 'v1'], ['a2', 'v2']])
58 |
59 | def test_print_table_no_rows(self):
60 | """Should return None when empty list of rows is passed"""
61 | self.assertIsNone(print_table([]))
62 |
63 | def test_print_table(self):
64 | """Should return table string to be printed"""
65 | self.assertEqual(
66 | print_table([['A', 'V'], ['a1', 'v1'], ['a2', 'v2']], is_pager=False),
67 | 'A V \n\na1 v1 \n\na2 v2 '
68 | )
69 |
70 | def test_print_response_503(self):
71 | """Should set color 'FAIL' and data when 503 code is provided"""
72 | self.assertEqual(print_response(code=503)[0], f'\x1b[91m{SERVER_DOWN_MSG}\x1b[0m')
73 |
74 | def test_print_response_404_400(self):
75 | """Should set color 'WARNING' when code is 404 / 400"""
76 | self.assertEqual(print_response(code=404, data='a')[0], '\x1b[93ma\x1b[0m')
77 | self.assertEqual(print_response(code=400, data='a')[0], '\x1b[93ma\x1b[0m')
78 |
79 | def test_print_response_401(self):
80 | """Should set color 'FAIL' and data when 401 code is provided"""
81 | self.assertEqual(print_response(code=401)[0], f'\x1b[91m{UNAUTHORIZED_MSG}\x1b[0m')
82 |
83 | def test_print_response_table(self):
84 | """Should set is_pager True when data_type table is provided with no pager in kwargs"""
85 | self.assertEqual(print_response(data_type='table', data=[['1', '2']])[0], '1 2 ')
86 |
87 | def test_print_response_no_data_no_extra(self):
88 | """Should return no data msg"""
89 | self.assertEqual(print_response()[0], '\x1b[93mNothing to show.\x1b[0m')
90 |
91 | def test_print_response_no_data(self):
92 | """Should return no data msg"""
93 | self.assertIsNone(print_response(extra='a')[0])
94 |
95 | def test_get_csrf_token_no_token(self):
96 | """Should return None when token not found in html"""
97 | html = HTML(html="")
98 | self.assertIsNone(get_csrf_token(html, "a"))
99 |
100 | def test_get_csrf_token_no_value(self):
101 | """Should return None when html element has no value"""
102 | html = HTML(html="")
103 | self.assertIsNone(get_csrf_token(html, "a"))
104 |
105 | def test_get_csrf_token(self):
106 | """Should return token from html element's value"""
107 | html = HTML(html="")
108 | self.assertEqual(get_csrf_token(html, "a"), 'b')
109 |
--------------------------------------------------------------------------------
/tests/test_problems.py:
--------------------------------------------------------------------------------
1 | from os import environ
2 | from platform import platform
3 | from unittest import TestCase
4 |
5 | from _pytest.monkeypatch import MonkeyPatch
6 |
7 | from codechefcli import problems
8 | from codechefcli.auth import LOGIN_FORM_ID
9 | from codechefcli.problems import (COMPILATION_ERROR_CLASS, INVALID_SOLUTION_ID_MSG,
10 | LANGUAGE_DROPDOWN_ID, LANGUAGE_SELECTOR, PAGE_INFO_CLASS,
11 | PROBLEM_SUBMISSION_FORM_ID, SOLUTION_ERR_MSG_CLASS,
12 | build_request_params, get_contest_problems, get_contests,
13 | get_description, get_ratings, get_solution, get_solutions,
14 | get_tags, search_problems, submit_problem)
15 | from tests.utils import HTML, MockHTMLResponse, fake_login
16 |
17 | temp_file_a = '/tmp/a'
18 | if 'Windows' in platform():
19 | temp_file_a = environ['TMP'] + r'\a'
20 |
21 |
22 | class ProblemsTestCase(TestCase):
23 | def setUp(self):
24 | self.monkeypatch = MonkeyPatch()
25 | fake_login()
26 |
27 | def test_get_problem_desc_invalid_json(self):
28 | """Should return 503 when response is not JSON-parsable"""
29 | def mock_req(*args, **kwargs):
30 | return MockHTMLResponse(json="{")
31 | self.monkeypatch.setattr(problems, "request", mock_req)
32 | self.assertEqual(get_description("a", "b")[0]['code'], 503)
33 |
34 | def test_get_problem_desc_err(self):
35 | """Should return 404 when problem api returns error status"""
36 | def mock_req(*args, **kwargs):
37 | return MockHTMLResponse(json='{"status": "error"}')
38 | self.monkeypatch.setattr(problems, "request", mock_req)
39 | resps = get_description("a", "b")
40 | self.assertEqual(resps[0]['code'], 404)
41 |
42 | def test_get_problem_desc_success(self):
43 | """Should return 404 when problem api returns error status"""
44 | def mock_req(*args, **kwargs):
45 | return MockHTMLResponse(
46 | json='{"status": "success", "problem_name": "a a", "body": "vbbv"}')
47 | self.monkeypatch.setattr(problems, "request", mock_req)
48 | resps = get_description("a", "b")
49 | self.assertEqual(
50 | resps[0]['data'],
51 | '\n\x1b[1mName: \x1b[0ma a\n\x1b[1mDescription:\x1b[0m\nvbbv\n\n\x1b[1mAuthor: \x1b[0m'
52 | '\n\x1b[1mDate Added: \x1b[0m\n\x1b[1mMax Time Limit: \x1b[0m secs\n\x1b[1mSource Limit'
53 | ': \x1b[0m Bytes\n\x1b[1mLanguages: \x1b[0m\n'
54 | )
55 |
56 | def test_submit_problem_no_login(self):
57 | """Should return 401 response when user is not logged in"""
58 | def mock_req(*args, **kwargs):
59 | return MockHTMLResponse(data=f"Login")
60 | self.monkeypatch.setattr(problems, "request", mock_req)
61 | self.assertEqual(submit_problem("A", "a/b", "p")[0]['code'], 401)
62 |
63 | def test_submit_problem_invalid_lang(self):
64 | """Should return 400 when invalid language is passed"""
65 | def mock_req(*args, **kwargs):
66 | return MockHTMLResponse(data=f" \
67 | \
72 | ")
73 | self.monkeypatch.setattr(problems, "request", mock_req)
74 | self.assertEqual(submit_problem("A", "a/b", "p")[0]['code'], 400)
75 |
76 | def test_submit_problem_sol_file_not_found(self):
77 | """Should return 400 response when solution file is not found"""
78 | def mock_req(*args, **kwargs):
79 | return MockHTMLResponse(data=f" \
80 | \
85 | ")
86 | self.monkeypatch.setattr(problems, "request", mock_req)
87 | self.assertEqual(submit_problem("A", "invalid_path/invalid_path", "a")[0]['code'], 400)
88 |
89 | def test_submit_problem_status_not_200(self):
90 | """Should return 503 response when status code is not 200"""
91 | def mock_req(*args, **kwargs):
92 | return MockHTMLResponse(status_code=400)
93 | self.monkeypatch.setattr(problems, "request", mock_req)
94 | self.assertEqual(submit_problem("A", "a/b", "p")[0]['code'], 503)
95 |
96 | def test_submit_problem_submission_status_not_200(self):
97 | """Should return 503 response when submission req status code is not 200"""
98 | def mock_req(*args, **kwargs):
99 | if kwargs.get('method') != 'POST':
100 | return MockHTMLResponse(data=f" \
101 | \
106 | ")
107 | return MockHTMLResponse(status_code=500)
108 | self.monkeypatch.setattr(problems, "request", mock_req)
109 |
110 | with open(temp_file_a, 'w') as f:
111 | f.write('a')
112 | self.assertEqual(submit_problem("A", temp_file_a, "a")[0]['code'], 503)
113 |
114 | def test_submit_problem_invalid_status_json(self):
115 | """Should return 503 response when submission status request returns invalid json"""
116 | def mock_req(*args, **kwargs):
117 | return MockHTMLResponse(data=f" \
118 | \
123 | ", json="{")
124 | self.monkeypatch.setattr(problems, "request", mock_req)
125 |
126 | with open(temp_file_a, 'w') as f:
127 | f.write('a')
128 | self.assertEqual(submit_problem("A", temp_file_a, "a")[0]['code'], 503)
129 |
130 | def test_submit_problem_compile_err(self):
131 | """Should return compilation error message when result code of submission is compile"""
132 | def mock_req(*args, **kwargs):
133 | return MockHTMLResponse(
134 | data=f" \
135 | \
140 | Comp Err",
141 | json='{"result_code": "compile"}'
142 | )
143 |
144 | def mock_get_status_table(*args, **kwargs):
145 | return
146 | self.monkeypatch.setattr(problems, "request", mock_req)
147 | self.monkeypatch.setattr(problems, "get_status_table", mock_get_status_table)
148 |
149 | with open(temp_file_a, 'w') as f:
150 | f.write('a')
151 | self.assertEqual(
152 | submit_problem("A", temp_file_a, "a")[0]['data'],
153 | '\x1b[91mCompilation error.\nComp Err\x1b[0m')
154 |
155 | def test_submit_problem_runtime_err(self):
156 | """Should return runtime error message when result code of submission is runtime"""
157 | def mock_req(*args, **kwargs):
158 | return MockHTMLResponse(data=f" \
159 | \
164 | ", json='{"result_code": "runtime", "signal": "abcd"}')
165 |
166 | def mock_get_status_table(*args, **kwargs):
167 | return
168 | self.monkeypatch.setattr(problems, "request", mock_req)
169 | self.monkeypatch.setattr(problems, "get_status_table", mock_get_status_table)
170 |
171 | with open(temp_file_a, 'w') as f:
172 | f.write('a')
173 | self.assertEqual(
174 | submit_problem(
175 | "A", temp_file_a, "a")[0]['data'], '\x1b[91mRuntime error. abcd\n\x1b[0m')
176 |
177 | def test_submit_problem_wrong_ans(self):
178 | """Should return wrong answer message when result code of submission is wrong"""
179 | def mock_req(*args, **kwargs):
180 | return MockHTMLResponse(data=f" \
181 | \
186 | ", json='{"result_code": "wrong"}')
187 |
188 | def mock_get_status_table(*args, **kwargs):
189 | return
190 | self.monkeypatch.setattr(problems, "request", mock_req)
191 | self.monkeypatch.setattr(problems, "get_status_table", mock_get_status_table)
192 |
193 | with open(temp_file_a, 'w') as f:
194 | f.write('a')
195 | self.assertEqual(
196 | submit_problem("A", temp_file_a, "a")[0]['data'], '\x1b[91mWrong answer\n\x1b[0m')
197 |
198 | def test_submit_problem_accepted_ans(self):
199 | """Should return accepted message when result code of submission is accepted"""
200 | def mock_req(*args, **kwargs):
201 | return MockHTMLResponse(data=f" \
202 | \
207 | ", json='{"result_code": "accepted"}')
208 |
209 | def mock_get_status_table(*args, **kwargs):
210 | return
211 | self.monkeypatch.setattr(problems, "request", mock_req)
212 | self.monkeypatch.setattr(problems, "get_status_table", mock_get_status_table)
213 |
214 | with open(temp_file_a, 'w') as f:
215 | f.write('a')
216 | self.assertEqual(submit_problem("A", temp_file_a, "a")[0]['data'], 'Correct answer\n')
217 |
218 |
219 | class SearchTestCase(TestCase):
220 | def setUp(self):
221 | self.monkeypatch = MonkeyPatch()
222 |
223 | def test_search_problems_status_not_200(self):
224 | """Should return 503 response code when req status is not 200"""
225 | def mock_req(*args, **kwargs):
226 | return MockHTMLResponse(status_code=400)
227 | self.monkeypatch.setattr(problems, 'request', mock_req)
228 | self.assertEqual(search_problems("Name", "asc", "easy")[0]['code'], 503)
229 |
230 | def test_search_problems_success(self):
231 | """Should return tabular response when status code 200"""
232 | def mock_req(*args, **kwargs):
233 | return MockHTMLResponse(data='
\
234 | AA BB \
235 | a1 b1 \
236 | a2 b2 \
237 |
')
238 | self.monkeypatch.setattr(problems, 'request', mock_req)
239 | self.assertEqual(
240 | search_problems("AA", "asc", "easy")[0]['data'],
241 | [['AA', 'BB', 'A1', 'B1', 'A2', 'B2'], ['a1', 'b1', 'a2', 'b2'], ['a2', 'b2']]
242 | )
243 |
244 | def test_contest_problems_invalid_json(self):
245 | """Should return 503 response when invalid json is received"""
246 | def mock_req(*args, **kwargs):
247 | return MockHTMLResponse(json="{")
248 | self.monkeypatch.setattr(problems, "request", mock_req)
249 | self.assertEqual(get_contest_problems("AA", "asc", "CC1")[0]['code'], 503)
250 |
251 | def test_contest_problems_api_error(self):
252 | """Should return 404 response when api response status is error"""
253 | def mock_req(*args, **kwargs):
254 | return MockHTMLResponse(json='{"status": "error"}')
255 | self.monkeypatch.setattr(problems, "request", mock_req)
256 | self.assertEqual(get_contest_problems("AA", "asc", "CC1")[0]['code'], 404)
257 |
258 | def test_contest_problems(self):
259 | """Should return contest problems info and table when status is success"""
260 | def mock_req(*args, **kwargs):
261 | return MockHTMLResponse(json='{ \
262 | "status": "success", \
263 | "name": "P1", \
264 | "announcements": "---", \
265 | "problems": { \
266 | "p1": { \
267 | "name": "P1", \
268 | "code": "p1", \
269 | "problem_url": "/p1", \
270 | "successful_submissions": 12, \
271 | "accuracy": "11", \
272 | "category_name": "main" \
273 | }, \
274 | "p2": { \
275 | "name": "P2", \
276 | "code": "p2", \
277 | "problem_url": "/p2", \
278 | "successful_submissions": 14, \
279 | "accuracy": "1", \
280 | "category_name": "" \
281 | } \
282 | } \
283 | }')
284 | self.monkeypatch.setattr(problems, "request", mock_req)
285 | resps = get_contest_problems("Name", "asc", "CC1")
286 | self.assertEqual(resps[0]['data'], '\n\x1b[1mName:\x1b[0m P1\n')
287 | self.assertEqual(
288 | resps[1]['data'], [
289 | ['NAME', 'CODE', 'URL', 'SUCCESSFUL SUBMISSIONS', 'ACCURACY', 'SCORABLE?'],
290 | ['P1', 'p1', 'https://www.codechef.com/p1', 12, '11 %', 'Yes'],
291 | ['P2', 'p2', 'https://www.codechef.com/p2', 14, '1 %', 'No']
292 | ]
293 | )
294 | self.assertEqual(resps[1]['data_type'], "table")
295 | self.assertEqual(resps[2]['data'], "\n\x1b[1mAnnouncements\x1b[0m:\n---")
296 |
297 |
298 | class TagsTestCase(TestCase):
299 | def setUp(self):
300 | self.monkeypatch = MonkeyPatch()
301 |
302 | def test_get_tags_invalid_json(self):
303 | """Should return 503 response when invalid json is received"""
304 | def mock_req(*args, **kwargs):
305 | return MockHTMLResponse(json='{')
306 | self.monkeypatch.setattr(problems, "request", mock_req)
307 | self.assertEqual(get_tags("a", "asc", [])[0]['code'], 503)
308 |
309 | def test_get_tags_status_not_200(self):
310 | """Should return 503 response when status code is not 200"""
311 | def mock_req(*args, **kwargs):
312 | return MockHTMLResponse(json='{"a": "A"}', status_code=400)
313 | self.monkeypatch.setattr(problems, "request", mock_req)
314 | self.assertEqual(get_tags("a", "asc", [])[0]['code'], 503)
315 |
316 | def test_get_tags(self):
317 | """Should return tags matrix"""
318 | def mock_req(*args, **kwargs):
319 | return MockHTMLResponse(
320 | json='[{"tag": "t1"}, {"tag": "t2"}, \
321 | {"tag": "t3"}, {"tag": "t4"}, {"tag": "t5"}, {"tag": "t6"}]')
322 | self.monkeypatch.setattr(problems, "request", mock_req)
323 | self.assertEqual(
324 | get_tags("a", "asc", [])[0]['data'], [['t1', 't2', 't3', 't4', 't5'], ['t6']])
325 |
326 | def test_get_tagged_problems_invalid_json(self):
327 | """Should return 503 response when invalid json is received"""
328 | def mock_req(*args, **kwargs):
329 | return MockHTMLResponse(json='{')
330 | self.monkeypatch.setattr(problems, "request", mock_req)
331 | self.assertEqual(get_tags("a", "asc", ["t1"])[0]['code'], 503)
332 |
333 | def test_get_tagged_problems_status_not_200(self):
334 | """Should return 503 response when status code is not 200"""
335 | def mock_req(*args, **kwargs):
336 | return MockHTMLResponse(json='{"a": "A"}', status_code=400)
337 | self.monkeypatch.setattr(problems, "request", mock_req)
338 | self.assertEqual(get_tags("a", "asc", ["t1"])[0]['code'], 503)
339 |
340 | def test_get_tagged_problems_no_data(self):
341 | """Should return table with tagged problems"""
342 | def mock_req(*args, **kwargs):
343 | return MockHTMLResponse(json='{"all_problems": null}')
344 | self.monkeypatch.setattr(problems, "request", mock_req)
345 | self.assertEqual(get_tags("a", "asc", ["t1"])[0]['code'], 404)
346 |
347 | def test_get_tagged_problems(self):
348 | """Should return table with tagged problems"""
349 | def mock_req(*args, **kwargs):
350 | return MockHTMLResponse(json='{ \
351 | "all_problems": { \
352 | "p1": {"code": "p1", "name": "P1", "attempted_by": 3, "solved_by": 2}, \
353 | "p2": {"code": "p2", "name": "P2", "attempted_by": 4, "solved_by": 4} \
354 | } \
355 | }')
356 | self.monkeypatch.setattr(problems, "request", mock_req)
357 | resps = get_tags("Name", "asc", ["t1"])
358 | self.assertEqual(resps[0]['data_type'], "table")
359 | self.assertEqual(
360 | resps[0]['data'], [
361 | ['CODE', 'NAME', 'SUBMISSION', 'ACCURACY'],
362 | ['p1', 'P1', '3', '66'],
363 | ['p2', 'P2', '4', '100']
364 | ]
365 | )
366 |
367 |
368 | class RatingsTestCase(TestCase):
369 | def setUp(self):
370 | self.monkeypatch = MonkeyPatch()
371 |
372 | def test_get_ratings_status_not_200(self):
373 | """Should return 503 response when status code is not 200"""
374 | def mock_req(*args, **kwargs):
375 | return MockHTMLResponse(status_code=400)
376 | self.monkeypatch.setattr(problems, "request", mock_req)
377 | self.assertEqual(get_ratings("a", "a", "a", "a", "a", "a", "a")[0]['code'], 503)
378 |
379 | def test_get_ratings_invalid_json(self):
380 | """Should return 503 response when invalid json is received"""
381 | def mock_req(*args, **kwargs):
382 | return MockHTMLResponse(json='{')
383 | self.monkeypatch.setattr(problems, "request", mock_req)
384 | self.assertEqual(get_ratings("a", "a", "a", "a", "a", "a", "a")[0]['code'], 503)
385 |
386 | def test_get_ratings_null_list(self):
387 | """Should return 404 response code when `list` key value has no elements"""
388 | def mock_req(*args, **kwargs):
389 | return MockHTMLResponse(json='{"list": null}')
390 | self.monkeypatch.setattr(problems, "request", mock_req)
391 | self.assertEqual(get_ratings("a", "a", "a", "a", "a", "a", "a")[0]['code'], 404)
392 |
393 | def test_get_ratings(self):
394 | """Should return table containing ratings"""
395 | def mock_req(*args, **kwargs):
396 | return MockHTMLResponse(json='{ \
397 | "list": [{ \
398 | "global_rank": 1, \
399 | "country_rank": 1, \
400 | "username": "u1", \
401 | "rating": 1, \
402 | "diff": 2 \
403 | }] \
404 | }')
405 | self.monkeypatch.setattr(problems, "request", mock_req)
406 | resps = get_ratings("RATING", "asc", "a", "a", "a", "a", "a")
407 | self.assertEqual(resps[0]['data_type'], "table")
408 | self.assertEqual(resps[0]['data'], [
409 | ['GLOBAL(COUNTRY)', 'USER NAME', 'RATING', 'GAIN/LOSS'], ['1 (1)', 'u1', '1', '2']])
410 |
411 |
412 | class ContestsTestCase(TestCase):
413 | def setUp(self):
414 | self.monkeypatch = MonkeyPatch()
415 |
416 | def test_get_contests_status_not_200(self):
417 | """Should return 503 response when status code is not 200"""
418 | def mock_req(*args, **kwargs):
419 | return MockHTMLResponse(status_code=400)
420 | self.monkeypatch.setattr(problems, "request", mock_req)
421 | self.assertEqual(get_contests(False)[0]['code'], 503)
422 |
423 | def test_get_contests_no_past(self):
424 | """Should return present & future contests"""
425 | def mock_req(*args, **kwargs):
426 | return MockHTMLResponse(data="
\
427 | \
428 | A B \
429 | a1 b1 \
430 |
\
431 | \
432 | Af Bf \
433 | af1 bf1 \
434 |
\
435 | ")
436 | self.monkeypatch.setattr(problems, "request", mock_req)
437 | resps = get_contests(False)
438 | self.assertEqual(resps[0]['data'], "\x1b[1mPresent Contests:\n\x1b[0m")
439 | self.assertEqual(resps[1]['data_type'], "table")
440 | self.assertEqual(resps[1]['data'], [['A', 'B'], ['a1', 'b1']])
441 | self.assertEqual(resps[2]['data'], "\x1b[1mFuture Contests:\n\x1b[0m")
442 | self.assertEqual(resps[3]['data_type'], "table")
443 | self.assertEqual(resps[3]['data'], [['AF', 'BF'], ['af1', 'bf1']])
444 |
445 | def test_get_contests_show_past(self):
446 | """Should return past contests"""
447 | def mock_req(*args, **kwargs):
448 | return MockHTMLResponse(data="
\
449 | \
450 | A B \
451 | a1 b1 \
452 |
\
453 | ")
454 | self.monkeypatch.setattr(problems, "request", mock_req)
455 | resps = get_contests(True)
456 | self.assertEqual(resps[0]['data'], '\x1b[1mPast Contests:\n\x1b[0m')
457 | self.assertEqual(resps[1]['data_type'], "table")
458 | self.assertEqual(resps[1]['data'], [['A', 'B'], ['a1', 'b1']])
459 |
460 |
461 | class SolutionsTestCase(TestCase):
462 | def setUp(self):
463 | self.monkeypatch = MonkeyPatch()
464 |
465 | def test_get_solutions_status_not_200(self):
466 | """Should return 503 response when status code is not 200"""
467 | def mock_req(*args, **kwargs):
468 | return MockHTMLResponse(status_code=400)
469 | self.monkeypatch.setattr(problems, "request", mock_req)
470 | self.assertEqual(get_solutions("a", "a", "a", "a", "a", "a", "a")[0]['code'], 503)
471 |
472 | def test_get_solutions_invalid_problem(self):
473 | """Should return 404 response when problem code is invalid"""
474 | def mock_req(*args, **kwargs):
475 | return MockHTMLResponse(url='/p2')
476 | self.monkeypatch.setattr(problems, "request", mock_req)
477 | self.assertEqual(get_solutions("A", "asc", "p1", 1, None, None, None)[0]['code'], 404)
478 |
479 | def test_get_solutions_no_filters(self):
480 | """Should return solutions of the problem (no filters)"""
481 | def mock_req(*args, **kwargs):
482 | return MockHTMLResponse(url='/p1', data='
\
483 | A B C D E \
484 | a1 b1 c1 d1 e1 \
485 |
')
486 | self.monkeypatch.setattr(problems, "request", mock_req)
487 | resps = get_solutions("A", "asc", "p1", 1, None, None, None)
488 | self.assertEqual(resps[0]['data_type'], "table")
489 | self.assertEqual(resps[0]['data'], [['A', 'B', 'C', 'D'], ['a1', 'b1', 'c1', 'd1']])
490 |
491 | def test_get_solutions_no_filters_with_page_info(self):
492 | """Should return solutions of the problem with page info (no filters)"""
493 | def mock_req(*args, **kwargs):
494 | return MockHTMLResponse(url='/p1', data=f'
\
495 | A B C D E \
496 | a1 b1 c1 d1 e1 \
497 |
111')
498 | self.monkeypatch.setattr(problems, "request", mock_req)
499 | resps = get_solutions("A", "asc", "p1", 1, None, None, None)
500 | self.assertEqual(resps[0]['data_type'], "table")
501 | self.assertEqual(resps[0]['data'], [['A', 'B', 'C', 'D'], ['a1', 'b1', 'c1', 'd1']])
502 | self.assertEqual(resps[0]['extra'], '\nPage: 111')
503 |
504 | def test_build_solution_filters(self):
505 | """Should return params dict containing solution filters"""
506 | params = build_request_params(
507 | HTML(html=f"'"), "a", "WA", "abcd", 2
512 | )
513 | self.assertEqual(
514 | params, {'language': 'a', 'page': 1, 'status': 14, 'handle': 'abcd'})
515 |
516 | def test_get_solution_status_not_200(self):
517 | """Should return 503 response when status code is not 200"""
518 | def mock_req(*args, **kwargs):
519 | return MockHTMLResponse(status_code=400)
520 | self.monkeypatch.setattr(problems, "request", mock_req)
521 | self.assertEqual(get_solution("a")[0]['code'], 503)
522 |
523 | def test_get_solution_not_found(self):
524 | """Should return 404 response when solution not found"""
525 | def mock_req(*args, **kwargs):
526 | return MockHTMLResponse(
527 | data=f'{INVALID_SOLUTION_ID_MSG}')
528 | self.monkeypatch.setattr(problems, "request", mock_req)
529 | self.assertEqual(get_solution("a")[0]['code'], 404)
530 |
531 | def test_get_solution(self):
532 | """Should return solution text"""
533 | def mock_req(*args, **kwargs):
534 | return MockHTMLResponse(data='print("hello cc")
')
535 | self.monkeypatch.setattr(problems, "request", mock_req)
536 | self.assertEqual(get_solution("a")[0]['data'], '\nprint("hello cc")\n')
537 |
--------------------------------------------------------------------------------
/tests/test_teams.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from _pytest.monkeypatch import MonkeyPatch
4 |
5 | from codechefcli import teams
6 | from tests.utils import MockHTMLResponse
7 |
8 |
9 | class TeamsTestCase(TestCase):
10 | def setUp(self):
11 | self.monkeypatch = MonkeyPatch()
12 |
13 | def test_get_team_no_name(self):
14 | """Should return empty list response when empty name is given"""
15 | self.assertEqual(teams.get_team(None), [])
16 |
17 | def test_get_team_status_not_200(self):
18 | """Should return 503 response code on any other status than 200 and 404"""
19 | def mock_req_team(*args, **kwargs):
20 | return MockHTMLResponse(status_code=500)
21 | self.monkeypatch.setattr(teams, "request", mock_req_team)
22 | self.assertEqual(teams.get_team("abcd")[0]["code"], 503)
23 |
24 | def test_get_team_status_404(self):
25 | """Should return 404 response code on 404 status code"""
26 | def mock_req_team(*args, **kwargs):
27 | return MockHTMLResponse(status_code=404)
28 | self.monkeypatch.setattr(teams, "request", mock_req_team)
29 | resps = teams.get_team("abcd")
30 | self.assertEqual(resps[0]["code"], 404)
31 | self.assertEqual(resps[0]["data"], "Team not found.")
32 |
33 | def test_get_team(self):
34 | """Should return team info"""
35 | def mock_req_team(*args, **kwargs):
36 | return MockHTMLResponse(data="
ABCD
\
37 | A: C \
38 | B: D \
39 | E: F \
40 | Information for G: G \
41 | xx \
42 |
\
43 | T U \
44 | t1 u1 \
45 | t2 u2 \
46 |
")
47 | self.monkeypatch.setattr(teams, "request", mock_req_team)
48 | resps = teams.get_team("abcd")
49 | self.assertEqual(
50 | resps[0]["data"],
51 | '\nABCD\n\nA: C\nB: D\nE: F\n\nInformation for G: G\n\nProblems Successfully Solved:\n'
52 | )
53 | self.assertListEqual(resps[1]["data"], [['T', 'U'], ['t1', 'u1'], ['t2', 'u2']])
54 |
--------------------------------------------------------------------------------
/tests/test_users.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from _pytest.monkeypatch import MonkeyPatch
4 |
5 | from codechefcli import users
6 | from codechefcli.users import (HEADER, RATING_NUMBER_CLASS, RATING_RANKS_CLASS, STAR_RATING_CLASS,
7 | USER_DETAILS_CLASS, USER_DETAILS_CONTAINER_CLASS, get_user)
8 | from tests.utils import MockHTMLResponse
9 |
10 |
11 | class UsersTestCase(TestCase):
12 | def setUp(self):
13 | self.monkeypatch = MonkeyPatch()
14 |
15 | def test_get_user_empty_username(self):
16 | """Should return empty list response"""
17 | self.assertEqual(get_user(None), [])
18 |
19 | def test_get_user_status_not_200(self):
20 | """Should return 503 response on any status code other than 200"""
21 | def mock_req_user(*args, **kwargs):
22 | return MockHTMLResponse(status_code=403)
23 | self.monkeypatch.setattr(users, "request", mock_req_user)
24 |
25 | resps = get_user("abc")
26 | self.assertEqual(resps[0]["code"], 503)
27 |
28 | def test_get_user_team_name(self):
29 | """Should return 400 response when username is of a team name"""
30 | def mock_req_user(*args, **kwargs):
31 | return MockHTMLResponse(url="/teams/view/abcd")
32 | self.monkeypatch.setattr(users, "request", mock_req_user)
33 |
34 | name = "abcd"
35 | resps = get_user(name)
36 |
37 | self.assertEqual(resps[0]["code"], 400)
38 | self.assertTrue(f'--team {name}' in resps[0]["data"])
39 |
40 | def test_get_user_not_found(self):
41 | """Should return 404 when the user is not found i.e. resp url is base url"""
42 | def mock_req_user(*args, **kwargs):
43 | return MockHTMLResponse()
44 | self.monkeypatch.setattr(users, "request", mock_req_user)
45 |
46 | resps = get_user("abcd")
47 | self.assertEqual(resps[0]["code"], 404)
48 | self.assertEqual(resps[0]["data"], "User not found.")
49 |
50 | def test_get_user(self):
51 | """Should return user info"""
52 | def mock_req_user(*args, **kwargs):
53 | return MockHTMLResponse(data=f" \
54 | <{HEADER}>ABCD's Profile{HEADER}> \
55 | \
56 | aa: 1 \
57 | bb: 2 \
58 | cc: 3 \
59 | dd: 4 \
60 | \
61 | 3star \
62 | \
63 | 1111 \
64 | ", url="/users/abcd/")
68 | self.monkeypatch.setattr(users, "request", mock_req_user)
69 |
70 | resps = get_user("abcd")
71 | self.assertEqual(
72 | resps[0]["data"],
73 | "\n\x1b[1mUser Details for ABCD's Profile (abcd):\x1b[0m\n\nbb: 2\ncc: 3\n"
74 | "User's Teams: https://www.codechef.com/users/abcd/teams/\n\nRating: 3star 1111\nGlobal"
75 | " Rank: 123\nCountry Rank: 11\n\nFind more at: https://www.codechef.com/users/abcd/\n"
76 | )
77 |
--------------------------------------------------------------------------------
/tests/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | from requests_html import HTML
5 |
6 | from codechefcli.helpers import BASE_URL, COOKIES_FILE_PATH
7 |
8 |
9 | class MockHTMLResponse:
10 | def __init__(self, data=' ', status_code=200, url='', json=""):
11 | self.html = HTML(html=data)
12 | self.status_code = status_code
13 | self.url = f'{BASE_URL}{url}'
14 | self.text = json
15 |
16 | def json(self, **kwargs):
17 | return json.loads(self.text)
18 |
19 |
20 | def fake_login(init_cookies=[]):
21 | """Fake login by creating cookies file having a fake cookie"""
22 | cookies = ''
23 | if init_cookies:
24 | cookies = "\n".join([
25 | f"Set-Cookie3: {cookie['name']}={cookie['value']}; path='/'; domain=localhost; \
26 | port=80000; expires='2120-05-05 23:40:21Z'; version=0" for cookie in init_cookies
27 | ])
28 | with open(COOKIES_FILE_PATH, 'w') as f:
29 | f.write(f'#LWP-Cookies-1.0\nSet-Cookie3: mykey=myvalue; path="/"; domain=localhost; port=80000; \
30 | expires="2120-05-05 23:40:21Z"; version=0\n{cookies}')
31 |
32 |
33 | def fake_logout():
34 | """Fake logout by deleting the cookies"""
35 | if os.path.exists(COOKIES_FILE_PATH):
36 | os.remove(COOKIES_FILE_PATH)
37 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [pep8]
2 | max-line-length = 120
--------------------------------------------------------------------------------