├── .flake8 ├── .gitignore ├── History.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── leetcodecli ├── __init__.py ├── cli.py └── header.html ├── setup.cfg └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include leetcodecli/header.html 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- 1 | name = "leetcodecli" 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /leetcodecli/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 35 | 36 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------