Skip to content

Commit ac54541

Browse files
committed
Move code to leetcode-export folder
1 parent c9c2a47 commit ac54541

File tree

10 files changed

+332
-205
lines changed

10 files changed

+332
-205
lines changed

README.md

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,21 @@
44
55
## DISCLAIMER
66

7-
All the problems hosted on leetcode.com are intellectual propriety of LeetCode, LLC. **DO NOT UPLOAD
8-
THE DESCRIPTION OF LEETCODE PROBLEMS ON GITHUB OR ON ANY OTHER WEBSITE** or you might receive ad DMCA Takedown notice.
7+
All the problems hosted on leetcode.com are intellectual propriety of LeetCode, LLC. **DO NOT UPLOAD THE DESCRIPTION OF
8+
LEETCODE PROBLEMS ON GITHUB OR ON ANY OTHER WEBSITE** or you might receive ad DMCA Takedown notice.
99

1010
Read [LeetCode Terms of Service here](https://leetcode.com/terms/).
1111

1212
To avoid committing the problem description on git, you can add `*.txt` to your `.gitignore` file.
1313

1414
## How it works
1515

16-
This script uses `selenium` and LeetCode APIs to download all your LeetCode submitted solutions.
16+
This script uses LeetCode REST and GraphQL APIs to download all your LeetCode submitted solutions.
1717

18-
Before running the script, make sure that python3, chrome and `chromedriver` are installed in your system.
18+
Before running the script, make sure that python3 is installed in your system.
1919

20-
If you do not want to configure and install all the required dependencies, you can download the Docker image. For
21-
further instructions read the section [Docker Image here](#docker-image).
22-
23-
`chromedriver` is used to get the cookies needed to download your LeetCode submissions. Alternatively, you can provide
24-
the cookies using the flag `--cookies` in the Python script.
20+
If you prefer, you can use the Docker image to download your submissions. For further instructions read the
21+
section [Docker Image here](#docker-image).
2522

2623
## How to use
2724

@@ -47,6 +44,49 @@ Now you can download all your LeetCode submission in the current folder by execu
4744
docker run -it -v $(pwd):/usr/app/out --rm nevermendel/leetcode-export
4845
```
4946

47+
## App parameters
48+
49+
The script accepts the following parameters:
50+
51+
```bash
52+
usage: app.py [-h] [--username USERNAME] [--password PASSWORD]
53+
[--folder FOLDER] [--cookies COOKIES] [-v] [-vv]
54+
55+
Export LeetCode solutions
56+
57+
optional arguments:
58+
-h, --help show this help message and exit
59+
--username USERNAME LeetCode username
60+
--password PASSWORD LeetCode password
61+
--folder FOLDER Output folder
62+
--cookies COOKIES LeetCode cookies
63+
-v, --verbose Enable verbose logging details
64+
-vv, --extra-verbose Enable more verbose logging details
65+
```
66+
67+
## Login
68+
69+
There are two ways to login in your LeetCode account, by providing username and password or by passing the cookies as program argument.
70+
71+
### Username and Password:
72+
73+
To login using username and password, insert them when prompted or pass them as parameter when lunching the
74+
script, like in the following example:
75+
76+
```bash
77+
python ./app.py --username {USERNAME} --password {PASSWORD}`
78+
```
79+
80+
The former option is to be preferred as it will avoid storing your password in the command history.
81+
82+
### Cookies
83+
84+
To login using cookies, pass the string containing them as parameter when lunching the script, like in the following example:
85+
86+
```bash
87+
python ./app.py --cookies {COOKIES}
88+
```
89+
5090
## License
5191

5292
[Apache License 2.0](LICENSE)

leetcode-export/app.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import getpass
4+
import logging
5+
import os
6+
from datetime import datetime
7+
from string import Template
8+
from typing import List, Dict
9+
10+
from leetcode import LeetCode
11+
from leetcode_rest import Submission
12+
from utils import language_to_extension, remove_special_characters
13+
14+
PROBLEM_INFO_TEMPLATE = Template('''${questionId} - ${title}
15+
${difficulty} - https://leetcode.com/problems/${titleSlug}/
16+
17+
${content}
18+
''')
19+
20+
21+
def parse_args():
22+
parser = argparse.ArgumentParser(description='Export LeetCode solutions')
23+
parser.add_argument('--username', type=str, required=False, help='LeetCode username')
24+
parser.add_argument('--password', type=str, required=False, help='LeetCode password')
25+
parser.add_argument('--folder', type=str, required=False, help='Output folder', default='out')
26+
parser.add_argument('--cookies', type=str, required=False, help='LeetCode cookies')
27+
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Enable verbose logging details')
28+
parser.add_argument('-vv', '--extra-verbose', dest='extra_verbose', action='store_true',
29+
help='Enable more verbose logging details')
30+
parser.set_defaults(verbose=False, extra_verbose=False)
31+
32+
return parser.parse_args()
33+
34+
35+
class App(object):
36+
args = parse_args()
37+
38+
# Set logging level based on program arguments
39+
level = logging.WARNING
40+
if args.verbose:
41+
level = logging.INFO
42+
if args.extra_verbose:
43+
level = logging.DEBUG
44+
45+
logging.basicConfig(
46+
level=level,
47+
format="%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d - %(message)s",
48+
handlers=[
49+
logging.FileHandler("debug.log"),
50+
logging.StreamHandler()
51+
]
52+
)
53+
54+
leetcode = LeetCode()
55+
56+
if not args.cookies:
57+
username = args.username
58+
password = args.password
59+
60+
if not username:
61+
username = input("Insert LeetCode username: ")
62+
if not password:
63+
password = getpass.getpass(prompt="Insert LeetCode password: ")
64+
if not leetcode.log_in(args.username, args.password):
65+
print(
66+
"Login not successful! You might have entered the wrong username/password or you need to complete the reCAPTCHA. If you need to complete the captcha, log in with the cookies instead. Check the log for more informaton.")
67+
exit(1)
68+
else:
69+
leetcode.set_cookies(args.cookies)
70+
71+
if not os.path.exists(args.folder):
72+
os.mkdir(args.folder)
73+
os.chdir(args.folder)
74+
75+
submissions: Dict[str, List[Submission]] = leetcode.get_submissions()
76+
77+
for slug in submissions:
78+
logging.info(f"Processing {slug}")
79+
if not os.path.exists(slug):
80+
os.mkdir(slug)
81+
else:
82+
logging.info(f"Folder {slug} already exists")
83+
os.chdir(slug)
84+
problem_info = leetcode.get_problem(slug)
85+
info_filename = f"{problem_info.questionId} - {slug}.txt"
86+
if not os.path.exists(info_filename):
87+
info_file = open(info_filename, 'w+')
88+
info_file.write(PROBLEM_INFO_TEMPLATE.substitute(**problem_info.__dict__))
89+
info_file.close()
90+
91+
for sub in submissions[slug]:
92+
sub_filename = f"{datetime.fromtimestamp(sub.timestamp).strftime('%Y-%m-%d %H.%M.%S')} - {sub.status_display} - runtime {remove_special_characters(sub.runtime)} - memory {remove_special_characters(sub.memory)}.{language_to_extension(sub.lang)}"
93+
if not os.path.exists(sub_filename):
94+
logging.info(f"Writing {slug}/{sub_filename}")
95+
sub_file = open(sub_filename, 'w+')
96+
sub_file.write(sub.code)
97+
sub_file.close()
98+
else:
99+
logging.info(f"{slug}/{sub_filename} already exists, skipping it")
100+
101+
os.chdir("..")

leetcode-export/leetcode.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import logging
2+
from time import sleep
3+
from typing import Dict, List
4+
5+
import requests
6+
7+
from leetcode_graphql import GRAPHQL_URL, question_detail_json, Problem
8+
from leetcode_rest import LOGIN_URL, SUBMISSIONS_API_URL, Submission, BASE_URL
9+
10+
11+
class LeetCode(object):
12+
def __init__(self):
13+
logging.debug("LeetCode class instantiated")
14+
self.cookies = ''
15+
self.user_logged = False
16+
17+
def log_in(self, username: str, password: str) -> bool:
18+
session = requests.Session()
19+
20+
session.get(LOGIN_URL)
21+
22+
csrftoken = session.cookies.get('csrftoken')
23+
24+
headers = {'Origin': BASE_URL, 'Referer': LOGIN_URL}
25+
payload = {'csrfmiddlewaretoken': csrftoken, 'login': username, 'password': password}
26+
27+
response_post = session.post(LOGIN_URL, headers=headers,
28+
data=payload) # sent using the same session, headers will also contain the cookies of the previous get request
29+
30+
if response_post.status_code == 200:
31+
self.cookies = f"csrftoken={session.cookies.get('csrftoken')};LEETCODE_SESSION={session.cookies.get('LEETCODE_SESSION')};"
32+
self.user_logged = True
33+
logging.info("Login successful")
34+
return True
35+
else:
36+
logging.warning(response_post.json())
37+
return False
38+
39+
def set_cookies(self, cookies: str):
40+
self.cookies = cookies
41+
self.user_logged = True
42+
logging.info("Cookies set successful")
43+
44+
def is_user_logged(self) -> bool:
45+
if self.user_logged:
46+
logging.debug("User is logged in")
47+
else:
48+
logging.debug("User is not logged in")
49+
return self.user_logged
50+
51+
def get_problem(self, slug: str) -> Problem:
52+
response = requests.post(
53+
GRAPHQL_URL,
54+
json=question_detail_json(slug),
55+
headers={'Cookie': self.cookies})
56+
if 'data' in response.json() and 'question' in response.json()['data']:
57+
return Problem.from_dict(response.json()['data']['question'])
58+
59+
def get_submissions(self) -> Dict[str, List[Submission]]:
60+
dictionary: Dict[str, List[Submission]] = {}
61+
current = 0
62+
response_json: Dict = {'has_next': True}
63+
while 'detail' not in response_json and 'has_next' in response_json and response_json['has_next']:
64+
logging.info(f"Exporting submissions from {current} to {current + 20}")
65+
response_json = requests.get(
66+
SUBMISSIONS_API_URL.format(current),
67+
headers={'Cookie': self.cookies}).json()
68+
if 'submissions_dump' in response_json:
69+
for submission_dict in response_json['submissions_dump']:
70+
submission = Submission.from_dict(submission_dict)
71+
if submission.title_slug not in dictionary:
72+
dictionary[submission.title_slug] = []
73+
dictionary[submission.title_slug].append(submission)
74+
sleep(1)
75+
current += 20
76+
if 'detail' in response_json:
77+
logging.warning(response_json['detail'])
78+
return dictionary
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from dataclasses import dataclass
2+
3+
from dataclasses_json import dataclass_json
4+
5+
GRAPHQL_URL = 'https://leetcode.com/graphql'
6+
7+
8+
@dataclass_json
9+
@dataclass
10+
class Problem:
11+
questionId: int
12+
difficulty: str
13+
title: str
14+
titleSlug: str
15+
content: str
16+
17+
18+
def question_detail_json(slug):
19+
return {
20+
"operationName": "getQuestionDetail",
21+
"variables": {
22+
"titleSlug": slug
23+
},
24+
"query": """query getQuestionDetail($titleSlug: String!) {
25+
question(titleSlug: $titleSlug) {
26+
questionId
27+
difficulty
28+
title
29+
titleSlug
30+
content
31+
}
32+
}"""
33+
}

leetcode-export/leetcode_rest.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from dataclasses import dataclass
2+
3+
from dataclasses_json import dataclass_json
4+
5+
BASE_URL = 'https://leetcode.com'
6+
LOGIN_URL = 'https://leetcode.com/accounts/login/'
7+
SUBMISSIONS_API_URL = 'https://leetcode.com/api/submissions/?offset={}&limit=20'
8+
PROBLEM_URL = 'https://leetcode.com/problems/'
9+
10+
11+
@dataclass_json
12+
@dataclass
13+
class Submission:
14+
id: int
15+
lang: str
16+
time: str
17+
timestamp: int
18+
status_display: str
19+
runtime: str
20+
url: str
21+
is_pending: str
22+
title: str
23+
memory: str
24+
code: str
25+
compare_result: str
26+
title_slug: str

leetcode-export/utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
FILE_EXTENSIONS = {
2+
"python": 'py',
3+
"python3": 'py',
4+
"c": 'c',
5+
"cpp": 'cpp',
6+
"csharp": 'cs',
7+
"java": 'java',
8+
"kotlin": 'kt',
9+
"mysql": 'sql',
10+
"mssql": 'sql',
11+
"oraclesql": 'sql',
12+
"javascript": 'js',
13+
"html": 'html',
14+
"php": 'php',
15+
"golang": 'go',
16+
"scala": 'scala',
17+
"pythonml": 'py',
18+
"rust": 'rs',
19+
"ruby": 'rb',
20+
"bash": 'sh',
21+
"swift": 'swift'
22+
}
23+
24+
SPECIAL_CHARACTERS_FILENAME = ['/', '\\', ':', '*', '?', '"', '"', '<', '>', ' ', '|']
25+
26+
27+
def language_to_extension(language: str) -> str:
28+
return FILE_EXTENSIONS[language]
29+
30+
31+
def remove_special_characters(string: str) -> str:
32+
for el in SPECIAL_CHARACTERS_FILENAME:
33+
string = string.replace(el, '')
34+
return string

0 commit comments

Comments
 (0)