Skip to content

Commit 7bd1b36

Browse files
committed
Change variable names of Problem class to snake_case and improve is_user_logged function to determine if user is logged based on get request result
1 parent 213c825 commit 7bd1b36

File tree

6 files changed

+107
-49
lines changed

6 files changed

+107
-49
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ RUN apk add --no-cache \
77
py3-pip
88

99
# Copy current directory to /usr/src/app
10-
ADD . /usr/src/app
11-
WORKDIR /usr/src/app
10+
ADD . /usr/app
11+
WORKDIR /usr/app
1212

1313
# Create out folder
1414
RUN mkdir -p out

README.md

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,39 @@ section [Docker Image here](#docker-image).
2323

2424
## How to use
2525

26-
### Run locally
26+
To use it locally you can either download it from pypi.org or you can clone this repository.
2727

28-
First install all the needed dependencies by executing:
28+
## Download from pypi.org
29+
30+
Execute `py install leetcode-export` to download it and install all the needed dependencies. You might need to use `py3`
31+
depending on how python is configured in your system.
32+
33+
### Clone the repository
34+
35+
Clone this repository:
36+
37+
```bash
38+
git clone https://github.com/NeverMendel/leetcode-exports
39+
```
40+
41+
Install all the needed dependencies:
2942

3043
```bash
3144
pip install -r requirements.txt
3245
```
3346

47+
Now you can either install it or just execute it:
48+
49+
- To install leetcode-export in your system:
50+
```bash
51+
python ./setup.py install
52+
```
53+
54+
- To run the project without installing it:
55+
```bash
56+
python -m leetcode_export --folder submissions
57+
```
58+
3459
### Docker Image
3560

3661
Download the docker image from the DockerHub repository:
@@ -49,7 +74,7 @@ docker run -it -v $(pwd):/usr/app/out --rm nevermendel/leetcode-export
4974

5075
The script accepts the following arguments:
5176

52-
```bash
77+
```
5378
usage: leetcode-export [-h] [--username USERNAME] [--password PASSWORD]
5479
[--folder FOLDER] [--cookies COOKIES] [-v] [-vv]
5580
[--problem-filename PROBLEM_FILENAME]
@@ -73,27 +98,31 @@ optional arguments:
7398
7499
## Login
75100
76-
To download your submissions you need to log in your LeetCode account. There are two ways to log in, by
101+
To download your submissions you need to log in your LeetCode account. There are two ways to log in, by inserting
77102
username/password or by cookies.
78103
79104
You can either use the interactive menu to supply the required information or you can pass them as program arguments
80105
when lunching the script.
81106
107+
The former option is to be preferred as it will avoid storing your password or your cookies in the command history.
108+
82109
### Username/Password
83110
84111
To log in using username and password, insert them using the interactive menu (preferred) or pass them as arguments when
85112
lunching the script, like in the following example:
86113
87114
```bash
88-
python leetcode-export --username {USERNAME} --password {PASSWORD}`
115+
python leetcode-export --username {USERNAME} --password {PASSWORD}
89116
```
90117

91-
The former option is to be preferred as it will avoid storing your password in the command history.
92-
93118
### Cookies
94119

95-
To log in using cookies, pass the string containing them as arguments when lunching the script, like in the following
96-
example:
120+
To log in using cookies, you first need to get them from a session where you are already logged in. Login in your
121+
browser, open the browser's Dev Tool, click on the Network tab and copy the cookie header that is sent when you visit
122+
any leetcode webpage.
123+
124+
You can insert the cookie string that you have just copied in the interactive menu (recommended) or you can pass it as a
125+
program argument when lunching the script, like in the following example:
97126

98127
```bash
99128
python leetcode-export --cookies {COOKIES}
@@ -114,11 +143,11 @@ The template can contain parameters that will later be replaced based on the Lee
114143
parameters are the following:
115144

116145
```python
117-
questionId: int
146+
question_id: int
118147
difficulty: str
119148
stats: str
120149
title: str
121-
titleSlug: str
150+
title_slug: str
122151
```
123152

124153
Default problem description filename template: `${questionId} - ${titleSlug}.txt`

leetcode_export/__main__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,23 @@
99
from leetcode_export.leetcode import LeetCode
1010
from leetcode_export.leetcode_rest import Submission
1111

12-
PROBLEM_CONTENT_TEMPLATE = Template('''${questionId} - ${title}
13-
${difficulty} - https://leetcode.com/problems/${titleSlug}/
12+
PROBLEM_CONTENT_TEMPLATE = Template('''${question_id} - ${title}
13+
${difficulty} - https://leetcode.com/problems/${title_slug}/
1414
1515
${content}
1616
''')
1717

1818

1919
def parse_args():
2020
parser = argparse.ArgumentParser(description='Export LeetCode solutions')
21-
parser.add_argument('--username', type=str, required=False, help='Set LeetCode username')
22-
parser.add_argument('--password', type=str, required=False, help='Set LeetCode password')
23-
parser.add_argument('--folder', type=str, required=False, help='Output folder', default='out')
24-
parser.add_argument('--cookies', type=str, required=False, help='Set LeetCode cookies')
21+
parser.add_argument('--username', type=str, help='Set LeetCode username')
22+
parser.add_argument('--password', type=str, help='Set LeetCode password')
23+
parser.add_argument('--cookies', type=str, help='Set LeetCode cookies')
24+
parser.add_argument('--folder', type=str, default='.', help='Output folder')
2525
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Enable verbose logging details')
2626
parser.add_argument('-vv', '--extra-verbose', dest='extra_verbose', action='store_true',
2727
help='Enable more verbose logging details')
28-
parser.add_argument('--problem-filename', type=str, default='${questionId} - ${titleSlug}.txt',
28+
parser.add_argument('--problem-filename', type=str, default='${question_id} - ${title_slug}.txt',
2929
help='Problem description filename format')
3030
parser.add_argument('--submission-filename', type=str,
3131
default='${date_formatted} - ${status_display} - runtime ${runtime} - memory ${memory}.${extension}',

leetcode_export/leetcode.py

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,84 @@
1+
import datetime
12
import logging
2-
from datetime import datetime
33
from time import sleep
44
from typing import Dict, List
55

66
import requests
77

88
from leetcode_export.leetcode_graphql import GRAPHQL_URL, question_detail_json, Problem
99
from leetcode_export.leetcode_rest import LOGIN_URL, SUBMISSIONS_API_URL, Submission, BASE_URL
10-
from leetcode_export.utils import language_to_extension, remove_special_characters
10+
from leetcode_export.utils import language_to_extension, remove_special_characters, dict_camelcase_to_snakecase
1111

1212

1313
class LeetCode(object):
1414
def __init__(self):
1515
logging.debug("LeetCode class instantiated")
16-
self.cookies = ''
16+
self.session = requests.Session()
1717
self.user_logged = False
18+
self.user_logged_expiration = datetime.datetime.now()
1819

1920
def log_in(self, username: str, password: str) -> bool:
20-
session = requests.Session()
21+
self.session.get(LOGIN_URL)
2122

22-
session.get(LOGIN_URL)
23-
24-
csrftoken = session.cookies.get('csrftoken')
23+
csrftoken = self.session.cookies.get('csrftoken')
2524

2625
headers = {'Origin': BASE_URL, 'Referer': LOGIN_URL}
2726
payload = {'csrfmiddlewaretoken': csrftoken, 'login': username, 'password': password}
2827

29-
response_post = session.post(LOGIN_URL, headers=headers,
30-
data=payload) # sent using the same session, headers will also contain the cookies of the previous get request
28+
response_post = self.session.post(LOGIN_URL, headers=headers,
29+
data=payload) # sent using the same session, headers will also contain the cookies of the previous get request
3130

32-
if response_post.status_code == 200:
33-
self.cookies = f"csrftoken={session.cookies.get('csrftoken')};LEETCODE_SESSION={session.cookies.get('LEETCODE_SESSION')};"
34-
self.user_logged = True
31+
if response_post.status_code == 200 and self.is_user_logged():
32+
# self.cookies = f"csrftoken={self.session.cookies.get('csrftoken')};LEETCODE_SESSION={self.session.cookies.get('LEETCODE_SESSION')};"
3533
logging.info("Login successful")
3634
return True
3735
else:
3836
logging.warning(response_post.json())
3937
return False
4038

4139
def set_cookies(self, cookies: str):
42-
self.cookies = cookies
43-
self.user_logged = True
44-
logging.info("Cookies set successful")
40+
cookies_list = cookies.split(';')
41+
cookies_list = map(lambda el: el.split('='), cookies_list)
42+
for cookies in cookies_list:
43+
self.session.cookies.set(cookies[0].strip(), cookies[1].strip())
44+
if self.is_user_logged():
45+
logging.info("Cookies set successful")
46+
else:
47+
logging.warning("Cookies set failed")
4548

4649
def is_user_logged(self) -> bool:
47-
if self.user_logged:
48-
logging.debug("User is logged in")
49-
else:
50-
logging.debug("User is not logged in")
51-
return self.user_logged
50+
if self.user_logged and datetime.datetime.now() > self.user_logged_expiration:
51+
return True
52+
cookie_dict = self.session.cookies.get_dict()
53+
if 'csrftoken' in cookie_dict and 'LEETCODE_SESSION' in cookie_dict:
54+
get_request = self.session.get(SUBMISSIONS_API_URL.format(0))
55+
if 'detail' not in get_request.json():
56+
logging.debug("User is logged in")
57+
self.user_logged = True
58+
self.user_logged_expiration = datetime.datetime.now() + datetime.timedelta(hours=5)
59+
return True
60+
logging.debug("User is not logged in")
61+
return False
5262

5363
def get_problem(self, slug: str) -> Problem:
54-
response = requests.post(
64+
response = self.session.post(
5565
GRAPHQL_URL,
56-
json=question_detail_json(slug),
57-
headers={'Cookie': self.cookies})
66+
json=question_detail_json(slug))
5867
if 'data' in response.json() and 'question' in response.json()['data']:
59-
return Problem.from_dict(response.json()['data']['question'])
68+
problem_dict = dict_camelcase_to_snakecase(response.json()['data']['question'])
69+
return Problem.from_dict(problem_dict)
6070

6171
def get_submissions(self) -> Dict[str, List[Submission]]:
72+
if not self.is_user_logged():
73+
logging.warning("Trying to get user submissions while user is not logged in")
74+
return {}
6275
dictionary: Dict[str, List[Submission]] = {}
6376
current = 0
6477
response_json: Dict = {'has_next': True}
6578
while 'detail' not in response_json and 'has_next' in response_json and response_json['has_next']:
6679
logging.info(f"Exporting submissions from {current} to {current + 20}")
67-
response_json = requests.get(
68-
SUBMISSIONS_API_URL.format(current),
69-
headers={'Cookie': self.cookies}).json()
80+
response_json = self.session.get(
81+
SUBMISSIONS_API_URL.format(current)).json()
7082
if 'submissions_dump' in response_json:
7183
for submission_dict in response_json['submissions_dump']:
7284
submission_dict['runtime'] = submission_dict['runtime'].replace(' ', '')

leetcode_export/leetcode_graphql.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
@dataclass_json
99
@dataclass
1010
class Problem:
11-
questionId: int
11+
question_id: int
1212
difficulty: str
1313
stats: str
1414
title: str
15-
titleSlug: str
15+
title_slug: str
1616
content: str
1717

1818

leetcode_export/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import re
2+
from typing import Dict
3+
14
FILE_EXTENSIONS = {
25
"python": 'py',
36
"python3": 'py',
@@ -22,6 +25,8 @@
2225
}
2326

2427
SPECIAL_CHARACTERS_FILENAME = ['/', '\\', ':', '*', '?', '"', '"', '<', '>', '|']
28+
_regex_camelcase_to_snakecase1 = re.compile(r'(.)([A-Z][a-z]+)')
29+
_regex_camelcase_to_snakecase2 = re.compile('([a-z0-9])([A-Z])')
2530

2631

2732
def language_to_extension(language: str) -> str:
@@ -32,3 +37,15 @@ def remove_special_characters(string: str) -> str:
3237
for el in SPECIAL_CHARACTERS_FILENAME:
3338
string = string.replace(el, '')
3439
return string
40+
41+
42+
def camelcase_to_snakecase(string: str) -> str:
43+
subbed = _regex_camelcase_to_snakecase1.sub(r'\1_\2', string)
44+
return _regex_camelcase_to_snakecase2.sub(r'\1_\2', subbed).lower()
45+
46+
47+
def dict_camelcase_to_snakecase(dictionary: Dict[str, any]) -> Dict[str, any]:
48+
new_dict: Dict[str, any] = {}
49+
for key in dictionary:
50+
new_dict[camelcase_to_snakecase(key)] = dictionary[key]
51+
return new_dict

0 commit comments

Comments
 (0)