diff --git a/README.md b/README.md index d4db68b..4c20618 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ options: -v, --version show program's version number and exit Commands: - {config,stats,list,problem,today,submission,submit} + {config,stats,list,problem,today,submission,submit,check} config Configure the CLI stats Display statistics list Display problem list @@ -38,4 +38,48 @@ Commands: today Display today's problem. submission Download submission code submit Submit code answer + check Check code answer on example test ``` + +## Example workflow +You can search for the problem in multiple ways: using lists, fetching question of the day, random question or using specific ID. + +Take problem `1`, show its contents in the terminal and create a file with the code snippet: +``` +leet problem 1 -fc +``` + +Now try to solve the problem. After that you can check the code against example test case: +``` +leet test +``` + +Then try to submit the solution and wait for the response: +``` +leet submit +``` + +## Screenshots +``` +> leet stats +``` + +![](/images/stats.png) + + +``` +> leet list +``` + +![](/images/list.png) + +``` +> leet problem --contents +``` +![](/images/today.png) + +``` +> leet submission 1 --show +``` +![](/images/submission.png) + diff --git a/images/list.png b/images/list.png new file mode 100644 index 0000000..12849bc Binary files /dev/null and b/images/list.png differ diff --git a/images/stats.png b/images/stats.png new file mode 100644 index 0000000..e0a9d26 Binary files /dev/null and b/images/stats.png differ diff --git a/images/submission.png b/images/submission.png new file mode 100644 index 0000000..eaa6ac0 Binary files /dev/null and b/images/submission.png differ diff --git a/images/today.png b/images/today.png new file mode 100644 index 0000000..e39e679 Binary files /dev/null and b/images/today.png differ diff --git a/leetcode/configuration.py b/leetcode/configuration.py index 632302c..11a5373 100644 --- a/leetcode/configuration.py +++ b/leetcode/configuration.py @@ -114,9 +114,10 @@ def check_session_validity(self): @property def csrf_cookie(self) -> str: - response = requests.get(url=self.host, - cookies={"LEETCODE_SESSION": self.session_id}) - return response.cookies["csrftoken"] + # response = requests.get(url=self.host, + # cookies={"LEETCODE_SESSION": self.session_id}) + # return response.cookies["csrftoken"] + return self.user_config.get('csrf_token') @csrf_cookie.setter def csrf_cookie(self, value: str): diff --git a/leetcode/main.py b/leetcode/main.py index 07a39d8..8f1f551 100644 --- a/leetcode/main.py +++ b/leetcode/main.py @@ -10,14 +10,7 @@ from leetcode.models.problem_by_id_slug import ProblemInfo from leetcode.models.submit import SendSubmission -# TODO: pipes support -# TODO: add a command to open the question in editor -# TODO: add a command to show the solution in the terminal -# TODO: add a command to show the solution in the browser # TODO: problem with import in synced code or code to submit -# TODO: random problem selector (from not accepted problems) -# TODO: check the changes in question_content and apply them to the code in other files -# TODO: use config without having to have a session def positive_integer(value): try: @@ -58,10 +51,15 @@ def main(): problem_parser.add_argument('id', type=positive_integer, help='Problem ID of the problem', default=0, nargs='?') problem_parser.add_argument('-b', '--browser', action='store_true', help='Open the page in browser.') problem_parser.add_argument('-f', '--file', action='store_true', help='Create a file with the problem content.') + problem_parser.add_argument('-c', '--contents', action='store_true', help='Display contents of the question in the terminal.') today_problem_parser = subparsers.add_parser('today', help="Display today's problem.") today_problem_parser.set_defaults(func=QuestionOfToday) + group_2 = today_problem_parser.add_mutually_exclusive_group() + group_2.add_argument('-b', '--browser', action='store_true', help='Open the page in browser.') + group_2.add_argument('-c', '--contents', action='store_true', help='Display contents of the question in the terminal.') + group_2.add_argument('-f', '--file', action='store_true', help='Create a file with the problem content.') submission_parser = subparsers.add_parser('submission', help="Download submission code") submission_parser.add_argument('id', type=int, help='ID of the problem.') @@ -70,19 +68,18 @@ def main(): submission_parser.set_defaults(func=SubmissionList) submission_parser = subparsers.add_parser('submit', help='Submit code answer') - submission_parser.add_argument('question_slug', type=str, help="Title slug of the question") submission_parser.add_argument('path', type=str, help='Path to the file with code answer') submission_parser.set_defaults(func=SendSubmission) - - group_2 = today_problem_parser.add_mutually_exclusive_group() - group_2.add_argument('-b', '--browser', action='store_true', help='Open the page in browser.') - group_2.add_argument('-c', '--contents', action='store_true', help='Display contents of the question in the terminal.') + submission_parser = subparsers.add_parser('check', help='Check code answer on example test') + submission_parser.add_argument('path', type=str, help='Path to the file with code answer') + submission_parser.set_defaults(func=SendSubmission) + args = parser.parse_args() if hasattr(args, 'func'): command_instance = args.func() - command_instance._execute(args) # call the private method __execute + command_instance._execute(args) else: print("Unknown command. Use 'leet --help' for available commands.") diff --git a/leetcode/models/graphql_problemset_question_list.py b/leetcode/models/graphql_problemset_question_list.py index c794f7f..5235a12 100644 --- a/leetcode/models/graphql_problemset_question_list.py +++ b/leetcode/models/graphql_problemset_question_list.py @@ -13,6 +13,8 @@ class Question(): difficulty: str frontendQuestionId: int questionId: int + paidOnly: bool + titleSlug: str total: int questions: List[Question] @@ -26,7 +28,9 @@ def from_dict(cls, data): status=item.get('status'), difficulty=item.get('difficulty'), frontendQuestionId=item.get('frontendQuestionId'), - questionId=item.get('questionId') + questionId=item.get('questionId'), + paidOnly=item.get('paidOnly'), + titleSlug=item.get('titleSlug'), ) for item in questions_data ] diff --git a/leetcode/models/graphql_question_of_today.py b/leetcode/models/graphql_question_of_today.py index 68ae1a8..2f561ff 100644 --- a/leetcode/models/graphql_question_of_today.py +++ b/leetcode/models/graphql_question_of_today.py @@ -1,6 +1,7 @@ from leetcode.models import * from leetcode.models.graphql_question_content import QuestionContent from leetcode.models.graphql_question_info_table import QuestionInfoTable +from leetcode.models.graphql_get_question_detail import GetQuestionDetail @dataclass class QueryResult(JSONWizard): @@ -40,6 +41,7 @@ def __init__(self): # Instance specific variables self.contentFlag: bool = False self.browserFlag: bool = False + self.fileFlag: bool = False self.title_slug: str = None self.data = None @@ -72,7 +74,11 @@ def _execute(self, args) -> None: self.data = self.leet_API.post_query(self.graphql_query) self.data = QueryResult.from_dict(self.data['data']) self.title_slug = self.data.question.titleSlug + self.show() + + if self.fileFlag: + self.create_submission_file(self.title_slug) def show(self) -> None: """ Shows the question information and content or opens the question in a browser. @@ -91,6 +97,22 @@ def show(self) -> None: self.open_in_browser(link) else: print(question_info_table) + + @classmethod + def create_submission_file(cls, title_slug: str) -> None: + """ Creates a file with the question content. + + Args: + title_slug (str): The title slug of the question. """ + + """ Add watermark to the file.""" + watermark_info = '# This file was created by pyleetcode-cli software.\n# Do NOT modify the name of the file.\n\n' + question = GetQuestionDetail(title_slug) + filename = f"{question.question_id}.{question.title_slug}.py" + with open(filename, 'w') as file: + file.write(watermark_info) + file.write(question.code_snippet) + console.print(f"File '{filename}' has been created.") def __parse_args(self, args) -> None: """ Parses the command line arguments. @@ -102,6 +124,9 @@ def __parse_args(self, args) -> None: self.browserFlag = True if getattr(args, 'contents'): self.contentFlag = True + if getattr(args, 'file'): + self.fileFlag = True + diff --git a/leetcode/models/problem_by_id_slug.py b/leetcode/models/problem_by_id_slug.py index 74ac74e..cd965eb 100644 --- a/leetcode/models/problem_by_id_slug.py +++ b/leetcode/models/problem_by_id_slug.py @@ -17,6 +17,8 @@ def __init__(self): # Instance specific variables self.browserFlag = False self.fileFlag = False + self.randomFlag = False + self.contentFlag = False self._question_id: int = None self._title_slug: str = None @@ -97,18 +99,17 @@ def _execute(self, args): choosen_number = randint(1, total) while True: list_instance = ProblemsetQuestionList({'status': 'NOT_STARTED'}, limit=1, skip=choosen_number - 1) - problem = list_instance.fetch_data()['problemsetQuestionList']['questions'][0] - if not problem['paidOnly']: + problem = list_instance.fetch_data(list_instance.params).questions[0] + if not problem.paidOnly: break choosen_number = randint(1, total) with Loader('Fetching problem contents...', ''): - question_info_table = QuestionInfoTable(problem['titleSlug']) - question_content = QuestionContent(problem['titleSlug']) + question_info_table = QuestionInfoTable(problem.titleSlug) + question_content = QuestionContent(problem.titleSlug) console.print(question_info_table) console.print(question_content) - - else: + elif getattr(args, 'id') != 0: try: with Loader('Fetching problem info...', ''): self.data = self.leet_api.get_request(self.API_URL) @@ -124,6 +125,8 @@ def _execute(self, args): console.print(f"{e.__class__.__name__}: {e}", style=ALERT) if self.fileFlag: self.create_submission_file(self.title_slug) + else: + console.print("Invalid ID has been provided. Please try again.", style=ALERT) @classmethod def create_submission_file(cls, title_slug: str = None) -> None: @@ -133,9 +136,11 @@ def create_submission_file(cls, title_slug: str = None) -> None: Args: title_slug (str): The title slug of the problem. """ + watermark_info = '# This file was created by pyleetcode-cli software.\n# Do NOT modify the name of the file.\n\n' question = GetQuestionDetail(title_slug) file_name = f"{question.question_id}.{question.title_slug}.py" with open(file_name, 'w') as file: + file.write(watermark_info) file.write(question.code_snippet) console.print(f"File '{file_name}' has been created.") @@ -146,12 +151,15 @@ def show(self): link = self.config.host + f'/problems/{self.title_slug}/' console.print(f'Link to the problem: {link}') self.open_in_browser(link) - else: + elif self.contentFlag: question_info_table = QuestionInfoTable(self.title_slug) console.print(question_info_table) question_content = QuestionContent(self.title_slug) console.print(question_content) - + else: + question_info_table = QuestionInfoTable(self.title_slug) + console.print(question_info_table) + def __parse_args(self, args) -> None: """ Parses the arguments passed to the query. @@ -161,6 +169,10 @@ def __parse_args(self, args) -> None: self.browserFlag = True if getattr(args, 'file'): self.fileFlag = True + if getattr(args, 'random'): + self.randomFlag = True + if getattr(args, 'contents'): + self.contentFlag = True @property def data(self): diff --git a/leetcode/models/submit.py b/leetcode/models/submit.py index b922f79..7e26a96 100644 --- a/leetcode/models/submit.py +++ b/leetcode/models/submit.py @@ -12,6 +12,7 @@ def __init__(self): super().__init__() self.title_slug = None self.path = None + self.command = None self.runcode = None self.submission_id = None @@ -34,19 +35,30 @@ def runcode_check_url(self): return f"https://leetcode.com/submissions/detail/{self.runcode}/check/" def parse_args(self, args): - self.title_slug = args.question_slug + import os + self.command = args.command self.path = args.path + + # Get the info from filename : 1234.title-slug.py + filename = os.path.basename(self.path) + self.title_slug = filename.split('.')[1] + + # check if such slug exists ProblemInfo.lookup_slug(self.title_slug) def _execute(self, args): try: - with Loader('Uploading submission...', ''): - self.parse_args(args) - self.execute_submission(self.title_slug, self.path) - - self.show_submission_info(self.submit_response) + self.parse_args(args) + if self.command == 'submit': + with Loader('Uploading submission...', ''): + submit_response = self.execute_submission(self.title_slug, self.path) + self.show_submission_info(submit_response) + elif self.command == 'check': + with Loader('Checking submission...', ''): + check_response = self.execute_check(self.title_slug, self.path) + self.show_check_info(check_response) except Exception as e: console.print(f"{e.__class__.__name__}: {e}", style=ALERT) @@ -81,16 +93,19 @@ def execute_check(self, title_slug, filename): response = requests.get(url=self.runcode_check_url, headers=self.config.headers, cookies=self.config.cookies) - self.show_check_info(response.json()) + return response.json() + def show_check_info(self, response): if response.get('run_success'): - print(f"Runtime: {response.get('status_runtime')}") - print(f"Answer: {response.get('correct_answer')}") - print(f"Expected: {response.get('expected_code_answer')}") - print(f"Got answer: {response.get('code_answer')}") + console.print("\n[bold green]✓ Check Passed[/bold green]\n") + console.print(f"[bold]Runtime:[/bold] {response.get('status_runtime')}") + console.print(f"[bold]Answer:[/bold] {response.get('correct_answer')}") + console.print(f"[bold]Expected:[/bold] {response.get('expected_code_answer')}") + console.print(f"[bold]Got answer:[/bold] {response.get('code_answer')}") else: - print(f"Exception: {response.get('status_msg')}") + console.print("\n[bold red]✗ Check Failed[/bold red]\n") + console.print(f"[bold]Exception:[/bold] {response.get('status_msg')}") def execute_submission(self, title_slug, filename): # In similar way execute clicking submit button on the leetcode website @@ -114,24 +129,24 @@ def execute_submission(self, title_slug, filename): response = requests.get(url=self.submit_check_url, headers=self.config.headers, cookies=self.config.cookies) - self.submit_response = response.json() + return response.json() def show_submission_info(self, response): if response.get('run_success'): status_msg = response.get('status_msg') if status_msg == 'Accepted': # If the solution is accepted - print(f"Status: [bold green]{status_msg}[/bold green] :tada:") - print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} test cases -> {response.get('status_runtime')}") + console.print(f"\n[bold green]✓ Submission Passed[/bold green] :tada:") + console.print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} test cases in {response.get('status_runtime')}") perc_evalutaion = SubmitEvaluation(f"{response.get('runtime_percentile'):.2f}", f"{response.get('memory_percentile'):.2f}") - print(perc_evalutaion) + console.print(perc_evalutaion) elif status_msg == 'Wrong Answer': # If the solution is wrong - print(f"Status: [bold red]{status_msg}[/bold red] :tada:") - print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} testcases") + console.print(f"\n[bold red]✗ Submission Failed[/bold red] :tada:") + console.print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} testcases") else: if response.get('status_msg') == 'Time Limit Exceeded': - print(f"Status: [bold red]{response.get('status_msg')}[/bold red] :alarm_clock:") - print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} testcases") + console.print(f"\n[bold red]✗ Submission Failed[/bold red] :alarm_clock:") + console.print(f"Passed {response.get('total_correct')}/{response.get('total_testcases')} testcases") elif response.get('status_msg') == 'Runtime Error': - print(f"Status: [bold red]{response.get('status_msg')}[/bold red]") - print(f"{response.get('runtime_error')}") \ No newline at end of file + console.print(f"\n[bold red]✗ Submission Failed[/bold red]") + console.print(f"{response.get('runtime_error')}") diff --git a/setup.py b/setup.py index fb1fc75..50052be 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ long_description = f.read() setup(name='pyleetcode-cli', - version='0.1.0', + version='0.2.0', description='A CLI tool to access LeetCode', long_description=long_description, long_description_content_type="text/markdown",