├── setup.cfg
├── .flake8
├── MANIFEST.in
├── leetcodecli
├── __init__.py
├── header.html
└── cli.py
├── .gitignore
├── History.md
├── Makefile
├── setup.py
├── LICENSE
└── README.md
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 120
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include leetcodecli/header.html
2 |
--------------------------------------------------------------------------------
/leetcodecli/__init__.py:
--------------------------------------------------------------------------------
1 | name = "leetcodecli"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | leetcode_cli.egg-info/
4 | *.pyc
5 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 |
2 | 0.0.6 / 2019-05-06
3 | ==================
4 |
5 | * Fixed cheat command
6 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build: clean
2 | @python setup.py sdist bdist_wheel
3 |
4 | release: build
5 | @twine upload dist/*
6 |
7 | clean:
8 | @rm -fr build/ dist/ leetcode_cli.egg-info/
9 |
10 | .PHONY: build clean release
11 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import setuptools
4 |
5 | with open("README.md", "r") as fh:
6 | long_description = fh.read()
7 |
8 | setuptools.setup(
9 | name="leetcode-cli",
10 | version="0.1.0",
11 | author="Pengcheng Chen",
12 | author_email="pengcheng.chen@gmail.com",
13 | description="LeetCode CLI",
14 | long_description=long_description,
15 | long_description_content_type="text/markdown",
16 | url="https://github.com/chenpengcheng/cli",
17 | packages=setuptools.find_packages(),
18 | classifiers=[
19 | "Programming Language :: Python :: 3",
20 | "License :: OSI Approved :: MIT License",
21 | "Operating System :: OS Independent",
22 | ],
23 | keywords="leetcode",
24 | include_package_data=True,
25 | install_requires=[
26 | "ascii_graph",
27 | "browser-cookie3",
28 | "beautifulsoup4",
29 | "pyexecjs",
30 | "requests",
31 | ],
32 | entry_points={
33 | "console_scripts": [
34 | "leetcode-cli=leetcodecli.cli:main",
35 | ],
36 | },
37 | )
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 The Python Packaging Authority
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/leetcodecli/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
35 |
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LeetCode CLI
2 |
3 | Start `tmux`, `vim` and `leetcode-cli`. Practice as many questions as you can:-)
4 |
5 | This tool is not affiliated with [LeetCode](https://leetcode.com).
6 |
7 | ## Install
8 |
9 | ### Mac OS X
10 |
11 | ```
12 | brew install node
13 | sudo easy_install leetcode-cli
14 | ```
15 |
16 | ### Linux
17 |
18 | ```
19 | sudo apt install nodejs
20 | sudo pip install leetcode-cli
21 | ```
22 |
23 | ## Usage
24 |
25 | The most common commands are: `cd`, `ls`, `pull`, `cat`, `check`, `push`, `cheat`, `clear` and `/`.
26 |
27 | ```
28 | $ leetcode-cli
29 |
30 | (\_/)
31 | =(^.^)=
32 | (")_(")
33 | 243 solved 17 failed 523 todo
34 |
35 | #:/> ?
36 | cat - Show test case(s).
37 | cd - Change problem(s).
38 | cheat - Find the best solution.
39 | check - Test the solution.
40 | chmod - Change programming language.
41 | clear - Clear screen.
42 | find - Find problems by keyword. Alias: /.
43 | limit - Limit the number of problems.
44 | login - Login into the online judge.
45 | ls - Show problem(s).
46 | print [keyword] - Print problems by keyword in HTML.
47 | pull [*] - Pull latest solution(s). '*': all solved problems.
48 | push - Submit the solution.
49 | su - Change session.
50 |
51 | A tag can refer to a topic (e.g. array) or a company (e.g. amazon).
52 | A keyword can be anything (including a tag).
53 | Commands and options can be completed via .
54 |
55 | #:/>
56 | ```
57 |
58 | Control+D to exit.
59 |
60 | ## Demo
61 |
62 | At the root (`/`) level. `ls` lists all the topics. `#` is for problems without a topic.
63 |
64 | ```
65 | #:/> ls
66 | 29 #
67 | 81 array
68 | 28 backtracking <- 28 problems todo in backtracking
69 | 5 binary-indexed-tree
70 | 35 binary-search
71 | 12 binary-search-tree
72 | 26 bit-manipulation
73 | 3 brainteaser
74 | 31 breadth-first-search
75 | 60 depth-first-search
76 | ...
77 | 13 sort
78 | 14 stack
79 | 62 string
80 | 3 topological-sort
81 | 53 tree
82 | 12 trie
83 | 21 two-pointers
84 | 9 union-find
85 | 242 solved 18 failed 523 todo
86 | ```
87 |
88 | `cd ` changes the current topic.
89 |
90 | ```
91 | #:/> cd heap
92 | #:/heap>
93 | ```
94 |
95 | At the topic level, `ls` lists the problems by difficulty level and acceptance rate. Levels are seperated by a blank line. At each level, the problems are listed in the order of acceptance rate.
96 | The marks: `*` means `todo`, `x` `failed`, none means `solved`.
97 |
98 | ```
99 | #:/heap> ls
100 | 355 design-twitter <- the hardest
101 | *719 find-k-th-smallest-pair-distance
102 | *836 race-car
103 | 23 merge-k-sorted-lists
104 | *218 the-skyline-problem
105 | *803 cheapest-flights-within-k-stops
106 |
107 | 295 find-median-from-data-stream <- medium level
108 | *895 shortest-path-to-get-all-keys
109 | 373 find-k-pairs-with-smallest-sums
110 | ...
111 | 215 kth-largest-element-in-an-array
112 | *692 top-k-frequent-words
113 | *794 swim-in-rising-water
114 |
115 | 378 kth-smallest-element-in-a-sorted-matrix <- easy level
116 | 347 top-k-frequent-elements
117 | 451 sort-characters-by-frequency
118 | *761 employee-free-time <- the easiest
119 | 11 solved 0 failed 17 todo
120 | ```
121 |
122 | `cd ` changes the current problem. Then `ls` shows the description.
123 |
124 | ```
125 | #:/heap> cd 23
126 | #:/heap/23-merge-k-sorted-lists> ls
127 | [Linked-List, Heap, Divide-And-Conquer, 8/20]
128 |
129 | Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.
130 | Example:
131 |
132 | Input:
133 | [
134 | 1->4->5,
135 | 1->3->4,
136 | 2->6
137 | ]
138 | Output: 1->1->2->3->4->4->5->6
139 | ```
140 |
141 | `pull` downloads the latest solution and sample test case from the online judge. If no solution was submitted, a boiler plate is used. The solution/boilerplate is saved in `./ws/.` and can be edited.
142 |
143 | ```
144 | #:/heap/23-merge-k-sorted-lists> pull
145 | ,___,
146 | [O.o] Replace working copy? (y/N)
147 | /)__)
148 | -"--"-y
149 | ws/23.py
150 | ```
151 | `cat` show the sample test case. It is saved in `./ws/tests.dat`. Test cases can be added to it and be used by `check`.
152 |
153 | ```
154 | #:/heap/23-merge-k-sorted-lists> cat
155 | ws/23.py << [[1,4,5],[1,3,4],[2,6]]
156 | ```
157 |
158 | Now that we have the problem description and the sample test case, start coding and test the solution locally.
159 |
160 | ```
161 | $ vim ./ws/23.py
162 | $ python ./ws/23.py
163 | ```
164 |
165 | The default programming language is `Python`. To change it, use `chmod `. Once the solution passes tests locally, we can `check` it with or `push` it to the online judge. `push` reports the runtime and number of tests passed.
166 |
167 | ```
168 | #:/heap/23-merge-k-sorted-lists> check
169 | Input: [[1,4,5],[1,3,4],[2,6]]
170 | Result: [1,1,2,3,4,4,5,6]
171 | Runtime: 20 ms
172 |
173 | #:/heap/23-merge-k-sorted-lists> push
174 | Runtime % ms
175 | ###############################################################################
176 | ** 0 48
177 | ***** 1 52
178 | ***************** 2 56
179 | ********************************************************************** 8 60
180 | *********************************************************************** 8 64*
181 | **************************************** 5 68
182 | *********************************************** 6 72
183 | *************************************************************** 7 76
184 | ************************************** 4 80
185 | ************************ 3 84
186 | **************** 2 88
187 | ************** 2 92
188 | ************ 1 96
189 | **************** 2 100
190 | ***************** 2 104
191 | **************** 2 108
192 | *********************** 3 112
193 | ******************************** 4 116
194 | ************************ 3 120
195 | *********************** 3 124
196 | *********************** 3 128
197 | ****************** 2 132
198 | ********** 1 136
199 | ********* 1 140
200 | Rank: 20.51%
201 | Result: 131/131 tests passed
202 | Runtime: 64 ms
203 | ```
204 |
205 | `/` searches for problems matching a tag (`airbnb`) or a keyword (e.g. `palindrome`)
206 |
207 | ```
208 | #:/heap/23-merge-k-sorted-lists> cd ..
209 | #:/heap> cd ..
210 | #:/> /airbnb
211 | 220 contains-duplicate-iii
212 | 68 text-justification
213 | 10 regular-expression-matching
214 | x212 word-search-ii
215 | 269 alien-dictionary
216 | *336 palindrome-pairs
217 | 2 add-two-numbers
218 | 23 merge-k-sorted-lists
219 | *190 reverse-bits
220 | *803 cheapest-flights-within-k-stops
221 |
222 | 227 basic-calculator-ii
223 | 160 intersection-of-two-linked-lists
224 | *221 maximal-square
225 | 385 mini-parser
226 | 219 contains-duplicate-ii
227 | 20 valid-parentheses
228 | *756 pour-water
229 | 42 trapping-rain-water
230 | 1 two-sum
231 | 198 house-robber
232 | 251 flatten-2d-vector
233 | 415 add-strings
234 | 202 happy-number
235 |
236 | 108 convert-sorted-array-to-binary-search-tree
237 | *787 sliding-puzzle
238 | *757 pyramid-transition-matrix
239 | 217 contains-duplicate
240 | *752 ip-to-cidr
241 | *761 employee-free-time
242 | 136 single-number
243 | 20 solved 1 failed 9 todo
244 |
245 | #:/>
246 | ```
247 |
248 | The solutions are saved in the `./ws/` directory.
249 |
250 | `print` generates a syntax-highlighted [HTML](http://www.spiderman.ly/all.html).
251 |
--------------------------------------------------------------------------------
/leetcodecli/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import cmd
4 | import contextlib
5 | import difflib
6 | import functools
7 | import inspect
8 | import json
9 | import os
10 | import random
11 | import re
12 | import sys
13 | import time
14 | from datetime import datetime
15 |
16 | import browser_cookie3
17 | import bs4
18 | import execjs
19 | import requests
20 | import webbrowser
21 |
22 |
23 | class Magic(object):
24 | bunnies = [
25 | """
26 | (\\(\\
27 | (='.')
28 | o(__")")""",
29 |
30 | """
31 | (\\__/)
32 | (='.'=)
33 | (")_(")""",
34 |
35 | """
36 | (\\_/)
37 | =(^.^)=
38 | (")_(")""",
39 |
40 | """
41 | (\\__/)
42 | (>'.'<)
43 | (")_(")""",
44 | ]
45 |
46 | def __init__(self):
47 | self.motd = random.choice(self.bunnies)[1:]
48 |
49 | def magic(self, msg):
50 | return self.__owl(msg)
51 |
52 | def __owl(self, msg):
53 | return """,___,\n[O.o] %s\n/)__)\n-"--"-""" % msg
54 |
55 |
56 | class Problem(object):
57 | def __init__(self, pid, slug, rate, freq, status=None):
58 | self.loaded = False
59 | self.pid = pid
60 | self.slug = slug
61 | self.rate = rate
62 | self.freq = freq
63 | self.status = status
64 | self.topics = []
65 | self.desc = self.code = self.test = self.html = ''
66 | self.record = History(slug)
67 |
68 | def __str__(self):
69 | if self.solved:
70 | s = ' '
71 | elif self.failed:
72 | s = 'x'
73 | else:
74 | s = '*'
75 | s += '%3d %s' % (self.pid, self.slug)
76 | return s
77 |
78 | @property
79 | def solved(self):
80 | return self.status == 'ac'
81 |
82 | @solved.setter
83 | def solved(self, x):
84 | self.status = 'ac' if x else 'notac'
85 |
86 | @property
87 | def failed(self):
88 | return self.status == 'notac'
89 |
90 | @property
91 | def todo(self):
92 | return not self.status
93 |
94 | @property
95 | def tags(self):
96 | L = list(filter(lambda x: x != '#', self.topics))
97 | if not self.todo:
98 | L.append(str(self.record))
99 | return ', '.join(L).title()
100 |
101 |
102 | class Solution(object):
103 | def __init__(self, pid, runtime, code):
104 | self.pid = pid
105 | self.runtime = runtime
106 | self.code = code
107 |
108 | def __str__(self):
109 | s = '[%d ms]\n' % self.runtime
110 | for i, l in enumerate(self.code.splitlines()):
111 | s += '%3d %s\n' % (i, l)
112 | return s
113 |
114 |
115 | class Result(object):
116 | def __init__(self, sid, result):
117 | self.sid = sid
118 | self.success = False
119 | self.fintime = None
120 |
121 | def split(s):
122 | return s.splitlines() if type(s) in [bytes, str] else s
123 |
124 | self.input = result.get('last_testcase', result.get('input', ''))
125 | self.output = split(result.get('code_output', ''))
126 | self.expected = split(result.get('expected_output', ''))
127 | self.debug = split(result.get('std_output'))
128 |
129 | self.result = result.get('code_answer', [])
130 | if not self.result:
131 | total = result.get('total_testcases')
132 | passed = result.get('total_correct')
133 | if total:
134 | self.result.append('%d/%d tests passed' % (passed, total))
135 |
136 | self.errors = []
137 | for e in ['compile_error', 'runtime_error', 'error']:
138 | m = result.get(e)
139 | if m:
140 | self.errors.append(m)
141 |
142 | status = result.get('status_code')
143 | if status == 10:
144 | self.success = True
145 | elif status == 12:
146 | self.errors.append('Memory Limit Exceeded')
147 | elif status == 13:
148 | self.errors.append('Output Limit Exceeded')
149 | elif status == 14:
150 | self.errors.append('Time limit Exceeded')
151 |
152 | ts = result.get('status_runtime', '').replace('ms', '').strip()
153 | self.runtime = int(ts) if ts.isdigit() else 0
154 |
155 | def __str__(self):
156 | limit = 25
157 | s = '\n'.join(self.errors)
158 | if s:
159 | s += '\n'
160 |
161 | if self.result:
162 | s += 'Result: '
163 | s += ', '.join(self.result) + '\n'
164 |
165 | if self.input:
166 | s += 'Input: ' + ','.join(self.input.splitlines()) + '\n'
167 |
168 | if self.output:
169 | s += 'Output:'
170 | s += '\n' if len(self.output) > 1 else ' '
171 | s += '\n'.join(self.output[: limit]) + '\n'
172 |
173 | if self.expected:
174 | s += 'Expected:'
175 | s += '\n' if len(self.expected) > 1 else ' '
176 | s += '\n'.join(self.expected[: limit]) + '\n'
177 |
178 | if self.debug:
179 | s += 'Debug:'
180 | s += '\n' if len(self.debug) > 1 else ' '
181 | s += '\n'.join(self.debug[: limit]) + '\n'
182 |
183 | if self.runtime:
184 | s += 'Runtime: %d ms' % self.runtime + '\n'
185 |
186 | if self.fintime:
187 | m, sec = self.fintime / 60, self.fintime % 60
188 | s += 'Finish Time: %d min %d sec' % (m, sec) + '\n'
189 |
190 | return s.strip('\n')
191 |
192 |
193 | class History(object):
194 | def __init__(self, slug):
195 | self.slug = slug
196 | self.submissions = []
197 | self.passed = 0
198 |
199 | @property
200 | def sid(self):
201 | for e in reversed(self.submissions):
202 | return e[0]
203 | return None
204 |
205 | @property
206 | def total(self):
207 | return len(self.submissions)
208 |
209 | def add(self, sid, lang, status, timestamp='Now'):
210 | if status == 'Accepted':
211 | self.passed += 1
212 | self.submissions.append((sid, lang, status, timestamp))
213 |
214 | def __str__(self):
215 | return '%d/%d' % (self.passed, self.total) if self.total else ''
216 |
217 |
218 | class Session(object):
219 | def __init__(self, sid, name, active):
220 | self.sid = sid
221 | self.name = name if name else '#'
222 | self.active = active
223 |
224 | def __str__(self):
225 | return '*' if self.active else '' + self.name
226 |
227 |
228 | class OJMixin(object):
229 | domain, url = 'leetcode.com', 'https://leetcode.com'
230 | langs = ['c', 'cpp', 'golang', 'java', 'javascript', 'python', 'scala']
231 | lang = 'python'
232 |
233 | @property
234 | def suffix(self):
235 | suffixes = {'golang': 'go', 'javascript': 'js', 'python': 'py'}
236 | return suffixes.get(self.lang, self.lang)
237 |
238 | @property
239 | def language(self):
240 | languages = {
241 | 'cpp': 'C++',
242 | 'csharp': 'C#',
243 | 'golang': 'Go',
244 | 'javascript': 'JavaScript'
245 | }
246 | return languages.get(self.lang, self.lang.title())
247 |
248 | session, loggedIn = requests.session(), False
249 |
250 | def login(self):
251 | self.session.cookies.update(browser_cookie3.load(domain_name=self.domain))
252 |
253 | if self.session.cookies.get('LEETCODE_SESSION'):
254 | self.loggedIn = True
255 | print('Welcome to %s!' % self.domain)
256 | else:
257 | self.loggedIn = False
258 | print('Please login into %s!' % self.domain)
259 | webbrowser.open_new_tab(self.url)
260 |
261 | def parse_sessions(self, resp):
262 | sd = {}
263 | for s in json.loads(resp.text).get('sessions', []):
264 | sid, name, active = s['id'], s['name'] or '#', s['is_active']
265 | sd[name] = Session(sid, name, active)
266 | return sd
267 |
268 | def get_sessions(self):
269 | url = self.url + '/session/'
270 | headers = {
271 | 'referer': url,
272 | 'content-type': 'application/json',
273 | 'x-csrftoken': self.session.cookies['csrftoken'],
274 | 'x-requested-with': 'XMLHttpRequest',
275 | }
276 | resp = self.session.post(url, json={}, headers=headers)
277 | return self.parse_sessions(resp)
278 |
279 | def create_session(self, name):
280 | url = self.url + '/session/'
281 | headers = {
282 | 'referer': url,
283 | 'content-type': 'application/json',
284 | 'x-csrftoken': self.session.cookies['csrftoken'],
285 | 'x-requested-with': 'XMLHttpRequest',
286 | }
287 | data = {'func': 'create', 'name': name, }
288 | resp = self.session.put(url, json=data, headers=headers)
289 | return self.parse_sessions(resp)
290 |
291 | def activate_session(self, sid):
292 | url = self.url + '/session/'
293 | headers = {
294 | 'referer': url,
295 | 'content-type': 'application/json',
296 | 'x-csrftoken': self.session.cookies['csrftoken'],
297 | 'x-requested-with': 'XMLHttpRequest',
298 | }
299 | data = {'func': 'activate', 'target': sid, }
300 | resp = self.session.put(url, json=data, headers=headers)
301 | return self.parse_sessions(resp)
302 |
303 | def get_tags(self):
304 | url = self.url + '/problems/api/tags/'
305 |
306 | resp = self.session.get(url)
307 | data = json.loads(resp.text)
308 |
309 | topics = {}
310 | for e in data.get('topics'):
311 | t = e.get('slug')
312 | ql = e.get('questions')
313 | topics[t] = ql
314 |
315 | companies = {}
316 | for e in data.get('companies'):
317 | c = e.get('slug')
318 | ql = e.get('questions')
319 | companies[c] = set(ql)
320 |
321 | return (topics, companies)
322 |
323 | def get_problems(self):
324 | # ps = 'algorithms/'
325 | ps = 'favorite_lists/top-interview-questions/'
326 | url = self.url + '/api/problems/' + ps
327 |
328 | resp = self.session.get(url)
329 |
330 | problems = {}
331 | for e in json.loads(resp.text).get('stat_status_pairs'):
332 | i = e.get('stat').get('question_id')
333 | s = e.get('stat').get('question__title_slug')
334 | a = e.get('stat').get('total_acs')
335 | n = e.get('stat').get('total_submitted')
336 | f = e.get('frequency')
337 | t = e.get('status')
338 | ar = float(a) / n if n else 0
339 | problems[i] = Problem(pid=i, slug=s, rate=ar, freq=f, status=t)
340 |
341 | return problems
342 |
343 | def strip(self, s):
344 | return s.replace('\r', '').encode('ascii', 'ignore').decode()
345 |
346 | def wrap(self, s):
347 | return '\n' + s.strip().rstrip() + '\n' * 2
348 |
349 | def get_problem(self, p):
350 | url = self.url + '/graphql/'
351 | referer = self.url + '/problems/%s/description/' % p.slug
352 | headers = {
353 | 'referer': referer,
354 | 'content-type': 'application/json',
355 | 'x-csrftoken': self.session.cookies['csrftoken'],
356 | }
357 | data = {
358 | 'query': """
359 | query getQuestionDetail( $titleSlug: String! ) {
360 | question( titleSlug: $titleSlug ) {
361 | content
362 | codeDefinition
363 | sampleTestCase
364 | }
365 | }
366 | """,
367 | 'variables': {
368 | 'titleSlug': p.slug
369 | },
370 | 'operationName': 'getQuestionDetail',
371 | }
372 | resp = self.session.post(url, json=data, headers=headers)
373 |
374 | q = json.loads(resp.text)['data']['question']
375 | soup = bs4.BeautifulSoup(q.get('content'), 'html.parser')
376 | p.html = self.strip(soup.prettify())
377 | p.desc = self.wrap(self.strip(soup.get_text()))
378 |
379 | for cs in execjs.eval(q['codeDefinition']):
380 | if cs.get('text') == self.language:
381 | p.code = self.strip(cs.get('defaultCode', ''))
382 | p.test = q.get('sampleTestCase')
383 |
384 | if not p.todo:
385 | p.code = self.get_latest_solution(p)
386 | p.record = self.get_history(p)
387 |
388 | p.loaded = bool(p.desc and p.test and p.code)
389 |
390 | def get_latest_solution(self, p):
391 | url = self.url + '/submissions/latest/'
392 | referer = self.url + '/problems/%s/description/' % p.slug
393 | headers = {
394 | 'referer': referer,
395 | 'content-type': 'application/json',
396 | 'x-csrftoken': self.session.cookies['csrftoken'],
397 | 'x-requested-with': 'XMLHttpRequest',
398 | }
399 | data = {
400 | 'qid': p.pid,
401 | 'lang': self.lang,
402 | }
403 |
404 | resp = self.session.post(url, json=data, headers=headers)
405 |
406 | try:
407 | code = self.strip(json.loads(resp.text).get('code'))
408 | except ValueError:
409 | code = ''
410 | return code
411 |
412 | def get_solution(self, pid, runtime):
413 | url = self.url + '/submissions/api/detail/%d/%s/%d/' % \
414 | (pid, self.lang, runtime)
415 |
416 | resp = self.session.get(url)
417 | data = json.loads(resp.text)
418 | code = data.get('code')
419 |
420 | return Solution(pid, runtime, code)
421 |
422 | def get_solutions(self, pid, sid, limit=10):
423 | url = self.url + '/submissions/detail/%s/' % sid
424 | js = r'var pageData =\s*(.*?);'
425 |
426 | resp = self.session.get(url)
427 |
428 | def diff(a, sl):
429 | for b in sl:
430 | r = difflib.SequenceMatcher(a=a.code, b=b.code).ratio()
431 | if r >= 0.9:
432 | return False
433 | return True
434 |
435 | solutions = []
436 | for s in re.findall(js, resp.text, re.DOTALL):
437 | v = execjs.eval(s)
438 | try:
439 | df = json.loads(v.get('runtimeDistributionFormatted'))
440 | if df.get('lang') == self.lang:
441 | for e in df.get('distribution')[: limit]:
442 | t = int(e[0])
443 | sln = self.get_solution(pid, t)
444 | if diff(sln, solutions):
445 | solutions.append(sln)
446 | break
447 | except ValueError:
448 | pass
449 |
450 | return solutions
451 |
452 | def get_solution_runtimes(self, sid):
453 | url = self.url + '/submissions/detail/%s/' % sid
454 | js = r'var pageData =\s*(.*?);'
455 |
456 | resp = self.session.get(url)
457 | runtimes = []
458 |
459 | for s in re.findall(js, resp.text, re.DOTALL):
460 | v = execjs.eval(s)
461 | try:
462 | df = json.loads(v.get('runtimeDistributionFormatted'))
463 | if df.get('lang') == self.lang:
464 | for t, p in df.get('distribution'):
465 | runtimes.append((int(t), float(p)))
466 | except ValueError:
467 | pass
468 |
469 | return runtimes
470 |
471 | def test_solution(self, p, code, tests='', full=False):
472 | if full:
473 | epUrl, sidKey = 'submit', 'submission_id'
474 | else:
475 | epUrl, sidKey = 'interpret_solution', 'interpret_id'
476 |
477 | url = self.url + '/problems/%s/%s/' % (p.slug, epUrl)
478 | referer = self.url + '/problems/%s/description/' % p.slug
479 | headers = {
480 | 'referer': referer,
481 | 'content-type': 'application/json',
482 | 'x-csrftoken': self.session.cookies['csrftoken'],
483 | 'x-requested-with': 'XMLHttpRequest',
484 | }
485 | data = {
486 | 'judge_type': 'large',
487 | 'lang': self.lang,
488 | 'test_mode': False,
489 | 'question_id': str(p.pid),
490 | 'typed_code': code,
491 | 'data_input': tests,
492 | }
493 |
494 | resp = self.session.post(url, json=data, headers=headers)
495 | try:
496 | sid = json.loads(resp.text).get(sidKey)
497 | result = self.get_result(sid)
498 | except ValueError:
499 | result = None
500 |
501 | return result
502 |
503 | def get_result(self, sid, timeout=30):
504 | url = self.url + '/submissions/detail/%s/check/' % sid
505 |
506 | for i in range(timeout):
507 | time.sleep(1)
508 | resp = self.session.get(url)
509 | data = json.loads(resp.text)
510 | if data.get('state') == 'SUCCESS':
511 | break
512 | else:
513 | data = {'error': '< network timeout >'}
514 |
515 | return Result(sid, data)
516 |
517 | def get_history(self, p):
518 | url = self.url + '/graphql/'
519 | referer = self.url + '/problems/%s/submissions/' % p.slug
520 | headers = {
521 | 'referer': referer,
522 | 'content-type': 'application/json',
523 | 'x-csrftoken': self.session.cookies['csrftoken'],
524 | }
525 | data = {
526 | 'query': """
527 | query Submissions( $offset: Int! $limit: Int! $lastKey: String $questionSlug: String!) {
528 | submissionList( offset: $offset limit: $limit lastKey: $lastKey questionSlug: $questionSlug) {
529 | submissions {
530 | id
531 | statusDisplay
532 | lang
533 | timestamp
534 | url
535 | }
536 | }
537 | }
538 | """,
539 | 'variables': {
540 | 'offset': 0,
541 | 'limit': 10,
542 | 'questionSlug': p.slug
543 | },
544 | 'operationName': 'Submissions',
545 | }
546 | resp = self.session.post(url, json=data, headers=headers)
547 |
548 | r = History(p.slug)
549 | try:
550 | for e in json.loads(resp.text)['data']['submissionList']['submissions']:
551 | sid = e.get('url').split('/')[3]
552 | lang = e.get('lang')
553 | s = e.get('statusDisplay')
554 | t = e.get('timestamp')
555 | r.add(sid=sid, lang=lang, status=s, timestamp=t)
556 | except TypeError:
557 | pass
558 |
559 | return r
560 |
561 |
562 | class Html(object):
563 | def __init__(self, p):
564 | self.p = p
565 |
566 | @staticmethod
567 | def header():
568 | with open('leetcodecli/header.html', 'r') as f:
569 | s = f.read()
570 | return s + ''
571 |
572 | @staticmethod
573 | def tail():
574 | return '
'
575 |
576 | @property
577 | def title(self):
578 | p = self.p
579 | s = '' + str(p.pid) + ' ' + p.slug.replace('-', ' ').title() + '
'
580 | if p.todo:
581 | s = '' + s + '
'
582 | elif p.failed or p.rate < 0.34:
583 | s = '' + s + '
'
584 | else:
585 | s = '' + s + '
'
586 | return s
587 |
588 | @property
589 | def tags(self):
590 | p = self.p
591 | return '' + p.tags + '
' if p.tags else ''
592 |
593 | @property
594 | def desc(self):
595 | return self.p.html
596 |
597 | @property
598 | def code(self):
599 | p = self.p
600 | return '' + p.code + '
' if p.solved else ''
601 |
602 | def __str__(self):
603 | return ''.join([self.title, self.tags, self.desc])
604 |
605 |
606 | def login_required(f):
607 | @functools.wraps(f)
608 | def wrapper(*args, **kwargs):
609 | cs = args[0]
610 | if cs.loggedIn:
611 | return f(*args, **kwargs)
612 | return wrapper
613 |
614 |
615 | class CodeShell(cmd.Cmd, OJMixin, Magic):
616 | sessions, ws = {}, os.path.expanduser("~/ws")
617 | topics, companies, problems, cheatsheet = {}, {}, {}, {}
618 | topic = pid = None
619 | xlimit = 0
620 |
621 | def __init__(self):
622 | import readline
623 | if 'libedit' in readline.__doc__:
624 | readline.parse_and_bind('bind ^I rl_complete')
625 | else:
626 | readline.parse_and_bind('tab: complete')
627 |
628 | cmd.Cmd.__init__(self)
629 | Magic.__init__(self)
630 | if not os.path.exists(self.ws):
631 | os.makedirs(self.ws)
632 |
633 | def precmd(self, line):
634 | line = line.lower()
635 | if line.startswith('/'):
636 | line = 'find ' + ' '.join(line.split('/'))
637 | self.xpid = self.pid
638 | return line
639 |
640 | def postcmd(self, stop, line):
641 | if self.pid != self.xpid:
642 | self.ts = datetime.now()
643 | return stop
644 |
645 | def emptyline(self):
646 | pass
647 |
648 | @property
649 | def prompt(self):
650 | return self.sname + ':' + self.cwd + '> '
651 |
652 | def complete_all(self, keys, text, line, start, end):
653 | prefix, suffixes = ' '.join(line.split()[1:]), []
654 |
655 | for t in sorted(keys):
656 | if t.startswith(prefix):
657 | i = len(prefix)
658 | suffixes.append(t[i:])
659 |
660 | return [text + s for s in suffixes]
661 |
662 | @property
663 | def sname(self):
664 | for s in self.sessions.values():
665 | if s.active:
666 | return s.name
667 | return '~'
668 |
669 | @property
670 | def cwd(self):
671 | wd = '/'
672 | if self.topic:
673 | wd += self.topic
674 | if self.pid:
675 | wd += '/%d-%s' % (self.pid, self.problems[self.pid].slug)
676 | return wd
677 |
678 | @property
679 | def pad(self):
680 | if self.pid:
681 | return '%s/%d.%s' % (self.ws, self.pid, self.suffix)
682 | else:
683 | return None
684 |
685 | @property
686 | def tests(self):
687 | return '%s/tests.dat' % self.ws
688 |
689 | def load(self, force=False):
690 | if not self.topics or force:
691 | self.topics, self.companies = self.get_tags()
692 | if not self.problems or force:
693 | self.problems = self.get_problems()
694 | pl = set(self.problems)
695 |
696 | for t, tpl in self.topics.items():
697 | for pid in tpl[:]:
698 | p = self.problems.get(pid)
699 | if p:
700 | p.topics.append(t)
701 | pl.discard(pid)
702 | else:
703 | tpl.remove(pid)
704 |
705 | self.topics['#'] = list(sorted(pl))
706 | map(lambda i: self.problems[i].topics.append('#'), pl)
707 |
708 | for c, cpl in self.companies.items():
709 | cpl -= set(filter(lambda i: i not in self.problems, cpl))
710 |
711 | @contextlib.contextmanager
712 | def count(self, pl):
713 | solved = failed = todo = 0
714 | for p in pl:
715 | if p.solved:
716 | solved += 1
717 | elif p.failed:
718 | failed += 1
719 | else:
720 | todo += 1
721 | yield
722 | if pl:
723 | print('%d solved %d failed %d todo' % (solved, failed, todo))
724 |
725 | def list(self, pl):
726 | with self.count(pl):
727 | r = [1, 0.45, 0.3]
728 | for p in sorted(pl, key=lambda p: (p.rate, p.pid)):
729 | if p.rate > r[-1]:
730 | print('')
731 | r.pop()
732 | print(' ', p)
733 |
734 | def top(self):
735 | with self.count(self.problems.values()):
736 | pass
737 |
738 | def limit(self, limit):
739 | def update(pd):
740 | for k in list(pd):
741 | pl = list(filter(lambda i: i not in ps, pd[k]))
742 | if pl:
743 | pd[k] = pl
744 | else:
745 | del pd[k]
746 |
747 | self.xlimit = limit
748 | if self.xlimit:
749 | ps = set(sorted(self.problems, key=lambda i: -int(self.problems[i].freq))[limit:])
750 | for pid in ps:
751 | del self.problems[pid]
752 | for pd in [self.topics, self.companies]:
753 | update(pd)
754 |
755 | def do_help(self, arg):
756 | methods = inspect.getmembers(CodeShell, predicate=inspect.isfunction)
757 | for key, method in methods:
758 | if key.startswith('do_'):
759 | name = key.split('_')[1]
760 | doc = method.__doc__
761 | if (not arg or arg == name) and doc:
762 | print(name, '\t', doc)
763 | print("""
764 | A tag can refer to a topic (e.g. array) or a company (e.g. amazon).
765 | A keyword can be anything (including a tag).
766 | Commands and options can be completed by .""")
767 |
768 | def do_login(self, unused=None):
769 | """\t\t- Login into the online judge."""
770 | self.login()
771 | self.load(force=True)
772 | self.limit(self.xlimit)
773 | self.topic = self.pid = None
774 | if self.loggedIn:
775 | print(self.motd)
776 | self.sessions = self.get_sessions()
777 | self.top()
778 |
779 | def complete_su(self, *args):
780 | return self.complete_all(self.sessions.keys(), *args)
781 |
782 | @login_required
783 | def do_su(self, name):
784 | """\t- Change session."""
785 | if name not in self.sessions:
786 | prompt = self.magic('Create session? (y/N)')
787 | try:
788 | c = input(prompt).lower() in ['y', 'yes']
789 | except EOFError:
790 | c = False
791 | if c:
792 | self.sessions = self.create_session(name)
793 |
794 | s = self.sessions.get(name)
795 | if s and not s.active:
796 | self.sessions = self.activate_session(s.sid)
797 | self.load(force=True)
798 | self.limit(self.xlimit)
799 |
800 | def complete_chmod(self, *args):
801 | return self.complete_all(self.langs, *args)
802 |
803 | def do_chmod(self, lang):
804 | """\t- Change programming language."""
805 | if lang in self.langs and lang != self.lang:
806 | self.lang = lang
807 | for p in self.problems.values():
808 | p.code = ''
809 | self.cheatsheet.clear()
810 | else:
811 | print(self.lang)
812 |
813 | def do_ls(self, unused=None):
814 | """\t\t- Show problem(s)."""
815 | if not self.topic:
816 | for t in sorted(self.topics.keys()):
817 | pl = self.topics[t]
818 | if pl:
819 | todo = 0
820 | for pid in pl:
821 | if not self.problems[pid].solved:
822 | todo += 1
823 | print(' ', '%3d' % todo, t)
824 | self.top()
825 |
826 | elif not self.pid:
827 | pl = [self.problems[i] for i in self.topics.get(self.topic)]
828 | self.list(pl)
829 | else:
830 | p = self.problems[self.pid]
831 | if not p.loaded:
832 | self.get_problem(p)
833 | if p.tags:
834 | print('[%s]' % p.tags)
835 | print(p.desc)
836 |
837 | def do_find(self, key):
838 | """\t- Find problems by keyword. Alias: /."""
839 | if key:
840 | if key in self.companies:
841 | def fn(p): return p.pid in self.companies[key]
842 | else:
843 | def fn(p): return p.slug.find(key) != -1
844 | pl = list(filter(fn, self.problems.values()))
845 | self.list(pl)
846 |
847 | def complete_cd(self, *args):
848 | if self.topic:
849 | keys = [str(i) for i in self.topics[self.topic]]
850 | else:
851 | keys = self.topics.keys()
852 | return self.complete_all(keys, *args)
853 |
854 | def do_cd(self, arg):
855 | """\t- Change problem(s)."""
856 | if arg == '..':
857 | if self.pid:
858 | self.pid = None
859 | elif self.topic:
860 | self.topic = None
861 | elif arg in self.topics:
862 | self.topic = arg
863 | elif arg.isdigit():
864 | pid = int(arg)
865 | if pid in self.problems:
866 | self.pid = pid
867 | topics = self.problems[pid].topics
868 | if self.topic not in topics:
869 | self.topic = topics[0]
870 |
871 | def do_cat(self, unused):
872 | """\t\t- Show test case(s)."""
873 | if self.pad and os.path.isfile(self.tests):
874 | with open(self.tests, 'r') as f:
875 | tests = f.read()
876 | print(self.pad, '<<', ', '.join(tests.splitlines()))
877 |
878 | def do_pull(self, arg):
879 | """[*]\t\t- Pull latest solution(s). '*': all solved problems."""
880 | sync = arg == '*'
881 |
882 | def writable(p):
883 | if sync:
884 | w = p.solved
885 | elif not os.path.isfile(self.pad):
886 | w = True
887 | else:
888 | prompt = self.magic('Replace working copy? (y/N)')
889 | try:
890 | w = input(prompt).lower() in ['y', 'yes']
891 | except EOFError:
892 | w = False
893 | return w
894 |
895 | pl, xpid = sorted(self.problems) if sync else [self.pid], self.pid
896 |
897 | for pid in pl:
898 | p = self.problems.get(pid)
899 | if p:
900 | if not p.loaded:
901 | self.get_problem(p)
902 |
903 | if writable(p):
904 | self.pid = pid
905 | with open(self.pad, 'w') as f:
906 | f.write(p.code)
907 | print(self.pad)
908 |
909 | with open(self.tests, 'w') as f:
910 | f.write(p.test)
911 | self.pid, self.ts = xpid, datetime.now()
912 |
913 | @login_required
914 | def do_check(self, unused):
915 | """\t\t- Test the solution."""
916 | p = self.problems.get(self.pid)
917 | if p and os.path.isfile(self.pad):
918 | with open(self.pad, 'r') as f:
919 | code = f.read()
920 | with open(self.tests, 'r') as tf:
921 | tests = tf.read()
922 | result = self.test_solution(p, code, tests)
923 | if result:
924 | print('Input: ', ', '.join(tests.splitlines()))
925 | print(result)
926 |
927 | @login_required
928 | def do_push(self, unused):
929 | """\t\t- Submit the solution."""
930 | def histogram(t, times, limit=25):
931 | try:
932 | from ascii_graph import Pyasciigraph
933 |
934 | r = 0
935 | for i in range(len(times)):
936 | t1, p = times[i]
937 | if t1 >= t:
938 | times[i] = (str(t1) + '*', p)
939 | break
940 | else:
941 | r += p
942 |
943 | g = Pyasciigraph(graphsymbol='*')
944 | for L in g.graph('Runtime' + 66 * ' ' + '% ms',
945 | times[1:limit]):
946 | print(L)
947 | print('Rank: %.2f%%' % r)
948 | except ImportError:
949 | pass
950 |
951 | p = self.problems.get(self.pid)
952 | if p and os.path.isfile(self.pad):
953 | with open(self.pad, 'r') as f:
954 | code = f.read()
955 | result = self.test_solution(p, code, full=True)
956 | if result:
957 | p.solved = result.success
958 | if p.solved:
959 | runtimes = self.get_solution_runtimes(result.sid)
960 | histogram(result.runtime, runtimes)
961 | result.fintime = (datetime.now() - self.ts).seconds
962 | status = 'Accepted'
963 | else:
964 | with open(self.tests, 'a+') as f:
965 | if f.read().find(result.input) == -1:
966 | f.write('\n' + result.input)
967 | status = 'Wrong Answer'
968 | p.record.add(sid=result.sid, lang=self.lang, status=status)
969 | print(result)
970 |
971 | @login_required
972 | def do_cheat(self, limit):
973 | """\t- Find the best solution."""
974 | p = self.problems.get(self.pid)
975 | if p:
976 | sid = p.record.sid
977 | cs = self.cheatsheet.get(p.pid, [])
978 | if not cs and sid:
979 | cs = self.get_solutions(p.pid, sid)
980 | self.cheatsheet[p.pid] = cs
981 |
982 | limit = 1 if not limit else int(limit)
983 | for c in cs[: limit]:
984 | print(c)
985 |
986 | def do_print(self, key):
987 | """[keyword]\t- Print problems by keyword in HTML."""
988 |
989 | def find(key):
990 | if key in self.topics:
991 | topics = {key: self.topics[key]}
992 | elif key in self.companies:
993 | topics = {key: self.companies[key]}
994 | else:
995 | topics = self.topics
996 | return topics
997 |
998 | def load(pids, x):
999 | pl = []
1000 | for i in pids:
1001 | p = self.problems[i]
1002 | if not p.loaded:
1003 | self.get_problem(p)
1004 | x += 1
1005 | sys.stdout.write('\r%d' % x)
1006 | sys.stdout.flush()
1007 | pl.append(p)
1008 | return pl
1009 |
1010 | def fname(key):
1011 | L = [self.sname, str(self.xlimit), key]
1012 | L = list(filter(lambda w: w and w not in ('0', '#'), L))
1013 | return '-'.join(L) or 'all'
1014 |
1015 | topics, printed = find(key), set()
1016 |
1017 | with open(self.ws + '/%s.html' % fname(key), 'w') as f:
1018 | f.write(Html.header())
1019 | for t, pids in sorted(topics.items()):
1020 | pl = load(pids, len(printed))
1021 | for p in sorted(pl, key=lambda p: (p.rate, p.pid)):
1022 | if p.pid not in printed:
1023 | f.write(str(Html(p)))
1024 | printed.add(p.pid)
1025 | f.write(Html.tail())
1026 | print()
1027 |
1028 | def do_clear(self, unused):
1029 | """\t\t- Clear screen."""
1030 | os.system('clear')
1031 |
1032 | def do_limit(self, limit):
1033 | """\t- Limit the number of problems."""
1034 | if limit.isdigit():
1035 | limit = int(limit)
1036 | if limit > self.xlimit > 0 or self.xlimit > limit == 0:
1037 | self.load(force=True)
1038 | self.limit(limit)
1039 | elif self.xlimit:
1040 | print(self.xlimit)
1041 |
1042 | def do_eof(self, arg):
1043 | return True
1044 |
1045 |
1046 | def main():
1047 | shell = CodeShell()
1048 | shell.do_login()
1049 | shell.cmdloop()
1050 |
1051 |
1052 | if __name__ == '__main__':
1053 | main()
1054 |
--------------------------------------------------------------------------------