diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..4e42b32 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,16 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +onlyLabels: + - answered +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false \ No newline at end of file diff --git a/.gitignore b/.gitignore index edaa8b7..acbe89a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea/ __pycache__ .DS_Store +venv/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a947e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Abhijeet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 37dc2f4..510efed 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,37 @@ https://competitive-coding-api.herokuapp.com/api/ ## Request Format https://competitive-coding-api.herokuapp.com/api/{platform_name}/{user_name} -### Example +### Example URL https://competitive-coding-api.herokuapp.com/api/codechef/abhijeet_ar +### Example Badges +[Shields](https://shields.io/) can create dynamically updated badges from a JSON source such as this API. More configuration options are also available in their section on [dynamic badges](https://shields.io/#dynamic-badge). + +Replace `` with your username on that platform. + +#### abhijeet_ar's profile on Codeforces +`https://img.shields.io/badge/dynamic/json?&color=1f8acb&logo=codeforces&label=Codeforces&url=https://competitive-coding-api.herokuapp.com/api/codeforces/&query=%24.&prefix=&style=for-the-badge&cacheSeconds=86400` + +Suggested use, +* `` = `rating` +* `` = `Rating%20` + +#### radix28_numb's profile on CodeChef +`https://img.shields.io/badge/dynamic/json?label=CodeChef&query=%24.global_rank&url=https://competitive-coding-api.herokuapp.com/api/codechef/&prefix=&logo=codechef&logoColor=f5f5dc&labelColor=7b5e47&style=for-the-badge&cacheSeconds=86400` + +Suggested use, +* `` = `global_rank`, `country_rank` or `rating` +* `` = `Rank%20`, country abbreviation (e.g., `US%20%23`) or `Rating%20` + +### Pro Tip 💡 +Use this [JSON Formatter Chrome Extension](https://chrome.google.com/webstore/detail/json-formatter/bcjindcccaagfpapjjmafapmmgkkhgoa?hl=en) to view in a structured format. + ## Platforms Available * Codeforces * Codechef * SPOJ * Interviewbit +* Leetcode (new) +* Atcoder -If you would like to leave a feedback or request a feature, please [open an issue](https://github.com/Abhijeet-AR/Smart-Interviews-Score-Tracker/issues) or feel free to PR. Do follow [these](https://help.github.com/articles/creating-a-pull-request/) instructions to make a valid PR. +If you would like to leave a feedback or request a feature, please [open an issue](https://github.com/Abhijeet-AR/Competitive_Programming_Score_API/issues) or feel free to PR. Do follow [these](https://help.github.com/articles/creating-a-pull-request/) instructions to make a valid PR. diff --git a/chromedriver b/chromedriver new file mode 100755 index 0000000..10b8fc0 Binary files /dev/null and b/chromedriver differ diff --git a/details_soup.py b/details_soup.py index 129484f..f257b3f 100644 --- a/details_soup.py +++ b/details_soup.py @@ -1,7 +1,15 @@ import json import re +# DO NOT import this after requests +import grequests import requests +import os + from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.common.action_chains import ActionChains + +from util import get_safe_nested_key class UsernameError(Exception): @@ -12,6 +20,10 @@ class PlatformError(Exception): pass +class BrokenChangesError(Exception): + pass + + class UserData: def __init__(self, username=None): self.__username = username @@ -27,11 +39,16 @@ def __codechef(self): soup = BeautifulSoup(page.text, 'html.parser') try: - rank = soup.find('div', class_='rating-number').text + rating = soup.find('div', class_='rating-number').text except AttributeError: raise UsernameError('User not Found') - rating = soup.find('span', class_='rating').text + stars = soup.find('span', class_='rating') + if stars: + stars = stars.text + + highest_rating_container = soup.find('div', class_='rating-header') + highest_rating = highest_rating_container.find_next('small').text.split()[-1].rstrip(')') rating_ranks_container = soup.find('div', class_='rating-ranks') rating_ranks = rating_ranks_container.find_all('a') @@ -39,24 +56,48 @@ def __codechef(self): global_rank = rating_ranks[0].strong.text country_rank = rating_ranks[1].strong.text + if global_rank != 'NA' and global_rank != 'Inactive': + global_rank = int(global_rank) + country_rank = int(country_rank) + def contests_details_get(): rating_table = soup.find('table', class_='rating-table') - + if not rating_table: + return [] rating_table_rows = rating_table.find_all('td') '''Can add ranking url to contests''' - long_challenge = {'name': 'Long Challenge', 'rating': int(rating_table_rows[1].text), - 'global_rank': int(rating_table_rows[2].a.hx.text), - 'country_rank': int(rating_table_rows[3].a.hx.text)} + try: + long_challenge = {'name': 'Long Challenge', 'rating': int(rating_table_rows[1].text), + 'global_rank': int(rating_table_rows[2].a.hx.text), + 'country_rank': int(rating_table_rows[3].a.hx.text)} + + except ValueError: + long_challenge = {'name': 'Long Challenge', 'rating': int(rating_table_rows[1].text), + 'global_rank': rating_table_rows[2].a.hx.text, + 'country_rank': rating_table_rows[3].a.hx.text} - cook_off = {'name': 'Cook-off', 'rating': int(rating_table_rows[5].text), - 'global_rank': int(rating_table_rows[6].a.hx.text), - 'country_rank': int(rating_table_rows[7].a.hx.text)} + try: + cook_off = {'name': 'Cook-off', + 'rating': int(rating_table_rows[5].text), + 'global_rank': int(rating_table_rows[6].a.hx.text), + 'country_rank': int(rating_table_rows[7].a.hx.text)} + except ValueError: + cook_off = {'name': 'Cook-off', + 'rating': int(rating_table_rows[5].text), + 'global_rank': rating_table_rows[6].a.hx.text, + 'country_rank': rating_table_rows[7].a.hx.text} - lunch_time = {'name': 'Lunch Time', 'rating': int(rating_table_rows[9].text), - 'global_rank': int(rating_table_rows[10].a.hx.text), - 'country_rank': int(rating_table_rows[11].a.hx.text)} + try: + lunch_time = {'name': 'Lunch Time', 'rating': int(rating_table_rows[9].text), + 'global_rank': int(rating_table_rows[10].a.hx.text), + 'country_rank': int(rating_table_rows[11].a.hx.text)} + + except ValueError: + lunch_time = {'name': 'Lunch Time', 'rating': int(rating_table_rows[9].text), + 'global_rank': rating_table_rows[10].a.hx.text, + 'country_rank': rating_table_rows[11].a.hx.text} return [long_challenge, cook_off, lunch_time] @@ -64,8 +105,12 @@ def contest_rating_details_get(): start_ind = page.text.find('[', page.text.find('all_rating')) end_ind = page.text.find(']', start_ind) + 1 - all_rating = json.loads(page.text[start_ind: end_ind]) + next_opening_brack = page.text.find('[', start_ind + 1) + while next_opening_brack < end_ind: + end_ind = page.text.find(']', end_ind + 1) + 1 + next_opening_brack = page.text.find('[', next_opening_brack + 1) + all_rating = json.loads(page.text[start_ind: end_ind]) for rating_contest in all_rating: rating_contest.pop('color') @@ -78,44 +123,92 @@ def problems_solved_get(): categories = problem_solved_section.find_all('article') - fully_solved = {'count': re.findall('\d+', no_solved[0].text)[0]} - for category in categories[0].find_all('p'): - category_name = category.find('strong').text[:-1] - fully_solved[category_name] = [] + fully_solved = {'count': int(re.findall(r'\d+', no_solved[0].text)[0])} - for prob in category.find_all('a'): - fully_solved[category_name].append({'name': prob.text, - 'link': 'https://www.codechef.com' + prob['href']}) + if fully_solved['count'] != 0: + for category in categories[0].find_all('p'): + category_name = category.find('strong').text[:-1] + fully_solved[category_name] = [] - partially_solved = {'count': re.findall('\d+', no_solved[1].text)[0]} - for category in categories[1].find_all('p'): - category_name = category.find('strong').text[:-1] - partially_solved[category_name] = [] - - for prob in category.find_all('a'): - partially_solved[category_name].append({'name': prob.text, + for prob in category.find_all('a'): + fully_solved[category_name].append({'name': prob.text, 'link': 'https://www.codechef.com' + prob['href']}) + partially_solved = {'count': int(re.findall(r'\d+', no_solved[1].text)[0])} + if partially_solved['count'] != 0: + for category in categories[1].find_all('p'): + category_name = category.find('strong').text[:-1] + partially_solved[category_name] = [] + + for prob in category.find_all('a'): + partially_solved[category_name].append({'name': prob.text, + 'link': 'https://www.codechef.com' + prob['href']}) + return fully_solved, partially_solved + def user_details_get(): + user_details_attribute_exclusion_list = {'username', 'link', 'teams list', 'discuss profile'} + + header_containers = soup.find_all('header') + name = header_containers[1].find('h1', class_="h2-style").text + + user_details_section = soup.find('section', class_='user-details') + user_details_list = user_details_section.find_all('li') + + user_details_response = {'name': name, 'username': user_details_list[0].text.split('★')[-1].rstrip('\n')} + for user_details in user_details_list: + attribute, value = user_details.text.split(':')[:2] + attribute = attribute.strip().lower() + value = value.strip() + + if attribute not in user_details_attribute_exclusion_list: + user_details_response[attribute] = value + + return user_details_response + full, partial = problems_solved_get() - details = {'status': 'Success', 'rank': int(rank), 'rating': rating, 'global_rank': int(global_rank), - 'country_rank': int(country_rank), 'contests': contests_details_get(), + details = {'status': 'Success', 'rating': int(rating), 'stars': stars, 'highest_rating': int(highest_rating), + 'global_rank': global_rank, 'country_rank': country_rank, + 'user_details': user_details_get(), 'contests': contests_details_get(), 'contest_ratings': contest_rating_details_get(), 'fully_solved': full, 'partially_solved': partial} return details def __codeforces(self): - url = 'https://codeforces.com/api/user.info?handles={}'.format(self.__username) + urls = { + "user_info": {"url": f'https://codeforces.com/api/user.info?handles={self.__username}'}, + "user_contests": {"url": f'https://codeforces.com/contests/with/{self.__username}'} + } - page = requests.get(url) + reqs = [grequests.get(item["url"]) for item in urls.values() if item.get("url")] - if page.status_code != 200: - raise UsernameError('User not Found') - - details_api = page.json() + responses = grequests.map(reqs) - if details_api['status'] != 'OK': + details_api = {} + contests = [] + for page in responses: + if page.status_code != 200: + raise UsernameError('User not Found') + if page.request.url == urls["user_info"]["url"]: + details_api = page.json() + elif page.request.url == urls["user_contests"]["url"]: + soup = BeautifulSoup(page.text, 'html.parser') + table = soup.find('table', attrs={'class': 'user-contests-table'}) + table_body = table.find('tbody') + + rows = table_body.find_all('tr') + for row in rows: + cols = row.find_all('td') + cols = [ele.text.strip() for ele in cols] + contests.append({ + "Contest": cols[1], + "Rank": cols[3], + "Solved": cols[4], + "Rating Change": cols[5], + "New Rating": cols[6] + }) + + if details_api.get('status') != 'OK': raise UsernameError('User not Found') details_api = details_api['result'][0] @@ -132,10 +225,16 @@ def __codeforces(self): rank = 'Unrated' max_rank = 'Unrated' - details = {'status': 'Success', 'username': self.__username, 'platform': 'Codeforces', - 'rating': rating, 'max rating': max_rating, 'rank': rank, 'max rank': max_rank} - - return details + return { + 'status': 'Success', + 'username': self.__username, + 'platform': 'Codeforces', + 'rating': rating, + 'max rating': max_rating, + 'rank': rank, + 'max rank': max_rank, + 'contests': contests + } def __spoj(self): url = 'https://www.spoj.com/users/{}/'.format(self.__username) @@ -150,6 +249,12 @@ def __spoj(self): join_date = details_container[1].text.split()[1] + ' ' + details_container[1].text.split()[2] institute = ' '.join(details_container[3].text.split()[1:]) + try: + points = float(points) + + except ValueError: + raise UsernameError('User not Found') + def get_solved_problems(): table = soup.find('table', class_='table table-condensed') @@ -202,6 +307,265 @@ def __interviewbit(self): return details + def __atcoder(self): + url = "https://atcoder.jp/users/{}".format(self.__username) + + page = requests.get(url) + + if page.status_code != 200: + raise UsernameError("User not Found") + + soup = BeautifulSoup(page.text, "html.parser") + tables = soup.find_all("table", class_="dl-table") + if len(tables) < 2: + details = { + "status": "Success", + "username": self.__username, + "platform": "Atcoder", + "rating": "NA", + "highest": "NA", + "rank": "NA", + "level": "NA", + } + return details + rows = tables[1].find_all("td") + try: + rank = int(rows[0].text[:-2]) + current_rating = int(rows[1].text) + spans = rows[2].find_all("span") + highest_rating = int(spans[0].text) + level = spans[2].text + except Exception as E: + raise BrokenChangesError(E) + details = { + "status": "Success", + "username": self.__username, + "platform": "Atcoder", + "rating": current_rating, + "highest": highest_rating, + "rank": rank, + "level": level, + } + return details + + # DEPRECATED + def __leetcode(self): + url = 'https://leetcode.com/{}'.format(self.__username) + + if requests.get(url).status_code != 200: + raise UsernameError('User not Found') + + options = webdriver.ChromeOptions() + options.binary_location = os.environ.get("GOOGLE_CHROME_BIN") + options.add_argument("--headless") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--no-sandbox") + + # driver = webdriver.PhantomJS(executable_path='./phantomjs') + + driver = webdriver.Chrome(options=options, executable_path=os.environ.get("CHROMEDRIVER_PATH")) + try: + driver.get(url) + + driver.implicitly_wait(10) + + hover_ranking = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[1]/div[2]/div/div[1]/div[3]/div') + ActionChains(driver).move_to_element(to_element=hover_ranking).perform() + + ranking = driver.find_element_by_xpath('/html/body/div[4]/div/div/div/div[2]').text + print('rank: ', ranking) + + total_problems_solved = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[2]/div/div[1]/div[1]/div[2]').text + + acceptance_rate_span_1 = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[2]/div/div[1]/div[2]/div[2]/div/div[1]/span[1]').text + acceptance_rate_span_2 = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[2]/div/div[1]/div[2]/div[2]/div/div[1]/span[2]').text + acceptance_rate = str(acceptance_rate_span_1) + str(acceptance_rate_span_2) + + easy_questions_solved = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[1]/div[2]/span[1]').text + total_easy_questions = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[1]/div[2]/span[2]').text + + medium_questions_solved = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[2]/div[2]/span[1]').text + total_medium_questions = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[2]/div[2]/span[2]').text + + hard_questions_solved = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[3]/div[2]/span[1]').text + total_hard_questions = driver.find_element_by_xpath( + '//*[@id="profile-root"]/div[2]/div/div[1]/div[2]/div/div[2]/div/div[3]/div[2]/span[2]').text + + contribution_points = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[3]/div[2]/div/div/div/li[1]/span').text + + contribution_problems = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[3]/div[2]/div/div/div/li[2]/span').text + + contribution_testcases = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[3]/div[2]/div/div/div/li[3]/span').text + + reputation = driver.find_element_by_xpath( + '/html/body/div[1]/div[2]/div/div[1]/div[4]/div[2]/div/div/div/li/span').text + finally: + driver.close() + driver.quit() + + details = {'status': 'Success', 'ranking': ranking[9:], + 'total_problems_solved': total_problems_solved, + 'acceptance_rate': acceptance_rate, + 'easy_questions_solved': easy_questions_solved, + 'total_easy_questions': total_easy_questions, + 'medium_questions_solved': medium_questions_solved, + 'total_medium_questions': total_medium_questions, + 'hard_questions_solved': hard_questions_solved, + 'total_hard_questions': total_hard_questions, + 'contribution_points': contribution_points, + 'contribution_problems': contribution_problems, + 'contribution_testcases': contribution_testcases, + 'reputation': reputation} + + return details + + def __leetcode_v2(self): + + def __parse_response(response): + total_submissions_count = 0 + total_easy_submissions_count = 0 + total_medium_submissions_count = 0 + total_hard_submissions_count = 0 + + ac_submissions_count = 0 + ac_easy_submissions_count = 0 + ac_medium_submissions_count = 0 + ac_hard_submissions_count = 0 + + total_easy_questions = 0 + total_medium_questions = 0 + total_hard_questions = 0 + + total_problems_solved = 0 + easy_questions_solved = 0 + medium_questions_solved = 0 + hard_questions_solved = 0 + + acceptance_rate = 0 + easy_acceptance_rate = 0 + medium_acceptance_rate = 0 + hard_acceptance_rate = 0 + + total_problems_submitted = 0 + easy_problems_submitted = 0 + medium_problems_submitted = 0 + hard_problems_submitted = 0 + + ranking = get_safe_nested_key(['data', 'matchedUser', 'profile', 'ranking'], response) + if ranking > 100000: + ranking = '~100000' + + reputation = get_safe_nested_key(['data', 'matchedUser', 'profile', 'reputation'], response) + + total_questions_stats = get_safe_nested_key(['data', 'allQuestionsCount'], response) + for item in total_questions_stats: + if item['difficulty'] == "Easy": + total_easy_questions = item['count'] + if item['difficulty'] == "Medium": + total_medium_questions = item['count'] + if item['difficulty'] == "Hard": + total_hard_questions = item['count'] + + ac_submissions = get_safe_nested_key(['data', 'matchedUser', 'submitStats', 'acSubmissionNum'], response) + for submission in ac_submissions: + if submission['difficulty'] == "All": + total_problems_solved = submission['count'] + ac_submissions_count = submission['submissions'] + if submission['difficulty'] == "Easy": + easy_questions_solved = submission['count'] + ac_easy_submissions_count = submission['submissions'] + if submission['difficulty'] == "Medium": + medium_questions_solved = submission['count'] + ac_medium_submissions_count = submission['submissions'] + if submission['difficulty'] == "Hard": + hard_questions_solved = submission['count'] + ac_hard_submissions_count = submission['submissions'] + + total_submissions = get_safe_nested_key(['data', 'matchedUser', 'submitStats', 'totalSubmissionNum'], + response) + for submission in total_submissions: + if submission['difficulty'] == "All": + total_problems_submitted = submission['count'] + total_submissions_count = submission['submissions'] + if submission['difficulty'] == "Easy": + easy_problems_submitted = submission['count'] + total_easy_submissions_count = submission['submissions'] + if submission['difficulty'] == "Medium": + medium_problems_submitted = submission['count'] + total_medium_submissions_count = submission['submissions'] + if submission['difficulty'] == "Hard": + hard_problems_submitted = submission['count'] + total_hard_submissions_count = submission['submissions'] + + if total_submissions_count > 0: + acceptance_rate = round(ac_submissions_count * 100 / total_submissions_count, 2) + if total_easy_submissions_count > 0: + easy_acceptance_rate = round(ac_easy_submissions_count * 100 / total_easy_submissions_count, 2) + if total_medium_submissions_count > 0: + medium_acceptance_rate = round(ac_medium_submissions_count * 100 / total_medium_submissions_count, 2) + if total_hard_submissions_count > 0: + hard_acceptance_rate = round(ac_hard_submissions_count * 100 / total_hard_submissions_count, 2) + + contribution_points = get_safe_nested_key(['data', 'matchedUser', 'contributions', 'points'], + response) + contribution_problems = get_safe_nested_key(['data', 'matchedUser', 'contributions', 'questionCount'], + response) + contribution_testcases = get_safe_nested_key(['data', 'matchedUser', 'contributions', 'testcaseCount'], + response) + + return { + 'status': 'Success', + 'ranking': str(ranking), + 'total_problems_submitted': str(total_problems_submitted), + 'total_problems_solved': str(total_problems_solved), + 'acceptance_rate': f"{acceptance_rate}%", + 'easy_problems_submitted': str(easy_problems_submitted), + 'easy_questions_solved': str(easy_questions_solved), + 'easy_acceptance_rate': f"{easy_acceptance_rate}%", + 'total_easy_questions': str(total_easy_questions), + 'medium_problems_submitted': str(medium_problems_submitted), + 'medium_questions_solved': str(medium_questions_solved), + 'medium_acceptance_rate': f"{medium_acceptance_rate}%", + 'total_medium_questions': str(total_medium_questions), + 'hard_problems_submitted': str(hard_problems_submitted), + 'hard_questions_solved': str(hard_questions_solved), + 'hard_acceptance_rate': f"{hard_acceptance_rate}%", + 'total_hard_questions': str(total_hard_questions), + 'contribution_points': str(contribution_points), + 'contribution_problems': str(contribution_problems), + 'contribution_testcases': str(contribution_testcases), + 'reputation': str(reputation) + } + + url = f'https://leetcode.com/{self.__username}' + if requests.get(url).status_code != 200: + raise UsernameError('User not Found') + payload = { + "operationName": "getUserProfile", + "variables": { + "username": self.__username + }, + "query": "query getUserProfile($username: String!) { allQuestionsCount { difficulty count } matchedUser(username: $username) { contributions { points questionCount testcaseCount } profile { reputation ranking } submitStats { acSubmissionNum { difficulty count submissions } totalSubmissionNum { difficulty count submissions } } }}" + } + res = requests.post(url='https://leetcode.com/graphql', + json=payload, + headers={'referer': f'https://leetcode.com/{self.__username}/'}) + res.raise_for_status() + res = res.json() + return __parse_response(res) + def get_details(self, platform): if platform == 'codechef': return self.__codechef() @@ -218,12 +582,26 @@ def get_details(self, platform): if platform == 'interviewbit': return self.__interviewbit() + if platform == 'leetcode': + return self.__leetcode_v2() + + if platform == 'atcoder': + return self.__atcoder() + raise PlatformError('Platform not Found') if __name__ == '__main__': - ud = UserData('abhijeet_ar') - - ans = ud.get_details('codechef') + ud = UserData('uwi') + ans = ud.get_details('leetcode') print(ans) + + # leetcode backward compatibility test. Commenting it out as it will fail in future + # leetcode_ud = UserData('saurabhprakash') + # leetcode_ans = leetcode_ud.get_details('leetcode') + # assert leetcode_ans == dict(status='Success', ranking='~100000', total_problems_solved='10', + # acceptance_rate='56.0%', easy_questions_solved='3', total_easy_questions='457', + # medium_questions_solved='5', total_medium_questions='901', hard_questions_solved='2', + # total_hard_questions='365', contribution_points='58', contribution_problems='0', + # contribution_testcases='0', reputation='0') diff --git a/main.py b/main.py index fcbe746..060a5f6 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,8 @@ from flask_restful import Api, Resource from flask_cors import CORS -from details_soup import UserData, UsernameError, PlatformError +from details_soup import UserData, UsernameError, PlatformError, BrokenChangesError +from send_mail import Mail app = Flask(__name__) CORS(app) @@ -22,6 +23,9 @@ def get(self, platform, username): except PlatformError: return {'status': 'Failed', 'details': 'Invalid Platform'} + + except BrokenChangesError: + return {'status': 'Failed', 'details': 'API broken due to site changes'} api.add_resource(Details,'/api//') @@ -33,4 +37,4 @@ def invalid_route(e): if __name__ == '__main__': - app.run() \ No newline at end of file + app.run() diff --git a/requirements.txt b/requirements.txt index f3f9e4c..74267db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,16 +4,19 @@ certifi==2019.11.28 chardet==3.0.4 Click==7.0 Flask==1.1.1 -Flask-Cors==3.0.8 +Flask-Cors==3.0.9 Flask-RESTful==0.3.7 +grequests==0.6.0 gunicorn==20.0.4 idna==2.8 itsdangerous==1.1.0 -Jinja2==2.11.1 +Jinja2==2.11.3 MarkupSafe==1.1.1 pytz==2019.3 -requests==2.22.0 +requests==2.27.1 +selenium==3.141.0 six==1.14.0 soupsieve==1.9.5 -urllib3==1.25.8 +urllib3==1.26.9 Werkzeug==0.16.1 +selenium==3.141.0 \ No newline at end of file diff --git a/send_mail.py b/send_mail.py new file mode 100644 index 0000000..126a906 --- /dev/null +++ b/send_mail.py @@ -0,0 +1,31 @@ +import smtplib +import datetime +import os + + +class Mail: + def __init__(self): + self.__timeStamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f%Z") + self.__server = smtplib.SMTP('smtp.gmail.com', 587) + + self.__server.ehlo() + self.__server.starttls() + self.__server.ehlo() + + self.__server.login(os.environ.get("g_mail"), os.environ.get("g_pass")) + + def send_bug_detected(self): + subject = 'Bug detected in Competitive Programming Score API' + body = 'Bug detected in Competitive Programming Score API at {date}.\n'.format(date=self.__timeStamp) + + body += '\nCheck logs for more information\n' + body += 'https://dashboard.heroku.com/apps/competitive-coding-api/logs' + + message = f'Subject : {subject}\n\n{body}\n' + + self.__server.sendmail('', ['abhijeet_abhi@live.co.uk'], message) + + +if __name__ == '__main__': + mail = Mail() + mail.send_bug_detected() diff --git a/util.py b/util.py new file mode 100644 index 0000000..36ec03b --- /dev/null +++ b/util.py @@ -0,0 +1,17 @@ +def get_safe_nested_key(keys, dictionary): + if not isinstance(dictionary, dict): + return None + if isinstance(keys, str): + return dictionary.get(keys) + if isinstance(keys, list): + if len(keys) == 1: + return dictionary.get(keys[0]) + if len(keys) > 1: + return get_safe_nested_key(keys[1:], dictionary.get(keys[0])) + return None + return None + + +if __name__ == '__main__': + assert get_safe_nested_key(['a', 'b', 'c'], {"a": {"b": {"c": "C"}}}) == "C" + assert get_safe_nested_key(['a', 'b', 'c'], {"a": "A", "b": "B", "c": "C"}) is None