-
-
Save rshmhrj/66bf43966246c7b134c64d4b55b40a95 to your computer and use it in GitHub Desktop.
Make LeetCode (MLC): Quickly Scaffold LeetCode Problems for Python https://rshmhrj.io/posts/2024/mlc/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| code_folder=./lc | |
| study_plan=150 | |
| test_folder=./tests | |
| test_file_name=test_lc.py | |
| filename_prefix=p | |
| filename_extension=py | |
| difficulty_name_easy=1_easy | |
| difficulty_name_medium=2_medium | |
| difficulty_name_hard=3_hard |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # mlc == make leetcode | |
| # creates a new file for solving leetcode problems | |
| # e.g. `mlc https://leetcode.com/problems/valid-palindrome` | |
| # current version number | |
| VERSION=0.0.2 | |
| # constants for colorized logs | |
| BLUE='\033[0;36m' # Blue | |
| RED='\033[0;31m' # Red | |
| GREEN='\033[0;32m' # Green | |
| YELLOW='\033[0;33m' # Yellow | |
| NC='\033[0m' # No Color | |
| # constant for valid URL format | |
| # https://leetcode.com/problems/valid-palindrome | |
| # https://leetcode.com/problems/valid-palindrome/description/?envType=study-plan-v2&envId=leetcode-75 | |
| urlFmtRE="https:\/\/leetcode\.com\/problems\/([a-z0-9-]+)*\/?.*" | |
| # set verbose logs to off by default | |
| verbose=0 | |
| # quick set verbose on if -v flag is found | |
| # while getopts "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" o; do case "${o}" in v) verbose=1 ;; *) ;; esac done | |
| # logging helper functions | |
| function log_info() { echo -e "${BLUE}""$1""${NC}"; } | |
| function log_debug() { [[ $verbose -eq 1 ]] && log_info "$1""${NC}"; } | |
| function log_value() { echo -e "${GREEN}""$1""${NC}"; } | |
| function log_warn() { echo -e "${YELLOW}""warn: $1""${NC}"; } | |
| function log_error() { echo -e "${RED}""error: $1""${NC}"; } | |
| function log_usage_short() { log_info "${GREEN}""\`mlc -u \"https://leetcode.com/problems/valid-palindrome\"\`"; } | |
| # VARIABLES | |
| # put your folder structure here | |
| # code_folder: location of where your leetcode solutions are | |
| # study_plan: which study plan are you following? LeetCode 150, LeetCode 75, etc.? This will be used for a subfolder within the main leetcode folder | |
| # test_folder: location of where your leetcode tests are | |
| # config: location of your config file | |
| config_file=~/.mlc | |
| code_folder=./lc | |
| study_plan=150 | |
| test_folder=./tests | |
| test_file_name=test_lc.py | |
| filename_prefix=p | |
| filename_extension=py | |
| difficulty_name_easy=1_easy | |
| difficulty_name_medium=2_medium | |
| difficulty_name_hard=3_hard | |
| . $config_file | |
| function log_usage() { | |
| log_info "creates a new file for solving leetcode problems" | |
| log_info " e.g. ${GREEN}""\`mlc -u \"https://leetcode.com/problems/valid-palindrome\"\`" | |
| log_info "${YELLOW}""valid options:" | |
| log_info "${GREEN}"" -u, --url\\t\\t""${BLUE}""leetcode problem url" | |
| log_info "\\t\\t""${BLUE}""usage:\\t${GREEN}""-u https://leetcode.com/problems/valid-palindrome" | |
| log_info "\\t\\t""${BLUE}""usage:\\t${GREEN}""--url https://leetcode.com/problems/valid-palindrome" | |
| log_info "\\t\\t""${BLUE}""${YELLOW}""note quotes around URL with ? in it:" | |
| log_info "\\t\\t""${BLUE}""usage:\\t${GREEN}""-u \"https://leetcode.com/problems/valid-palindrome/description/?envType=study-plan-v2&envId=leetcode-75\"" | |
| log_info "\\t\\t""${BLUE}""usage:\\t${GREEN}""--url \"https://leetcode.com/problems/valid-palindrome/description/?envType=study-plan-v2&envId=leetcode-75\"" | |
| log_info "${GREEN}"" -c, --config\\t\\t""${BLUE}""scaffold config file in ""${GREEN}""\`${config_file}\`""${BLUE}""." | |
| log_info "${GREEN}"" -s, --scaffold\\t""${BLUE}""scaffold folder structure" | |
| log_info "${GREEN}"" -p, --study_plan\\t""${BLUE}""leetcode study plan, e.g. ""${GREEN}""\`150\`""${BLUE}"" or ""${GREEN}""\`75\`""${BLUE}"". Defaults to ""${GREEN}""150""${BLUE}"". Currently set to ""${GREEN}""$study_plan""${BLUE}""." | |
| log_info "${GREEN}"" -f, --code_folder\\t""${BLUE}""desired lc folder in which to add scaffolded problem, e.g. ""${GREEN}""\`./leetcode\`""${BLUE}"". Defaults to ""${GREEN}""\`./lc\`""${BLUE}"". Currently set to ""${GREEN}""$code_folder""${BLUE}""." | |
| log_info "${GREEN}"" -t, --test_folder\\t""${BLUE}""desired test folder to use to add scaffolded tests, e.g. ""${GREEN}""\`./tests\`""${BLUE}"". Defaults to ""${GREEN}""\`./tests\`""${BLUE}"". Currently set to ""${GREEN}""$test_folder""${BLUE}""." | |
| log_info "${GREEN}"" -v, --verbose\\t\\t""${BLUE}""verbose mode. Most useful if flag is set at the beginning of the command" | |
| log_info "${GREEN}"" -V, --version\\t\\t""${BLUE}""print current mlc script version" | |
| log_info "${GREEN}"" -h, --help\\t\\t""${BLUE}""print this help message" | |
| } | |
| # if no args are passed, print usage and exit | |
| if [ $# -eq 0 ]; then | |
| log_usage | |
| exit 0 | |
| fi | |
| scaffold_config_file() { | |
| cat <<EOF | |
| code_folder=./lc | |
| study_plan=150 | |
| test_folder=./tests | |
| test_file_name=test_lc.py | |
| filename_prefix=p | |
| filename_extension=py | |
| difficulty_name_easy=1_easy | |
| difficulty_name_medium=2_medium | |
| difficulty_name_hard=3_hard | |
| EOF | |
| } | |
| scaffold_test_case_file() { | |
| cat <<EOF | |
| import sys | |
| import unittest | |
| sys.path.insert(0, "$code_folder") | |
| sys.path.append("$code_folder/$study_plan/${difficulty_name_easy}") | |
| sys.path.append("$code_folder/$study_plan/${difficulty_name_medium}") | |
| sys.path.append("$code_folder/$study_plan/${difficulty_name_hard}") | |
| sys.path.append("$test_folder/data") | |
| if __name__ == "__main__": | |
| unittest.main() | |
| EOF | |
| } | |
| scaffold_folder_structure() { | |
| if [ ! -d "$code_folder" ]; then | |
| log_warn "$code_folder folder missing" | |
| mkdir -p "$code_folder"/"$study_plan"/"${difficulty_name_easy}" ./"$code_folder"/"$study_plan"/"${difficulty_name_medium}" ./"$code_folder"/"$study_plan"/"${difficulty_name_hard}" | |
| log_info " created folder structure" | |
| fi | |
| if [ ! -d "$code_folder"/"$study_plan"/"${difficulty_name_easy}" ]; then | |
| mkdir -p "$code_folder"/"$study_plan"/"${difficulty_name_easy}" | |
| fi | |
| if [ ! -d "$code_folder"/"$study_plan"/"${difficulty_name_medium}" ]; then | |
| mkdir -p "$code_folder"/"$study_plan"/"${difficulty_name_medium}" | |
| fi | |
| if [ ! -d "$code_folder"/"$study_plan"/"${difficulty_name_hard}" ]; then | |
| mkdir -p "$code_folder"/"$study_plan"/"${difficulty_name_hard}" | |
| fi | |
| if [ ! -d "$test_folder" ]; then | |
| log_warn "$test_folder folder missing" | |
| mkdir -p "$test_folder"/data | |
| scaffold_test_case_file >>"$test_folder"/${test_file_name} | |
| log_info " created test folder structure" | |
| fi | |
| } | |
| # parse command arguments | |
| while [[ $# -gt 0 ]]; do | |
| log_debug "parsing: $1: ""${GREEN}""$2" | |
| # shift makes you parse next argument as $1. | |
| # shift n makes you move n arguments ahead. | |
| case $1 in | |
| -v | --verbose) | |
| verbose=1 | |
| log_debug "verbose mode on" | |
| log_debug "code_folder: ""${GREEN}""$code_folder" | |
| log_debug "test_folder: ""${GREEN}""$test_folder" | |
| log_debug "study_plan: ""${GREEN}""$study_plan" | |
| shift | |
| ;; | |
| -V | --version) | |
| log_info "mlc version: ""${GREEN}""$VERSION" | |
| exit 0 | |
| ;; | |
| -c | --config) | |
| log_debug "scaffolding default config file..." | |
| scaffold_config_file >"$config_file" | |
| exit 0 | |
| ;; | |
| -p | --study_plan) | |
| log_debug "$1: ""${GREEN}""$2" | |
| study_plan=$2 | |
| log_debug "study plan: ""${GREEN}""$study_plan" | |
| shift 2 | |
| ;; | |
| -f | --code_folder) | |
| log_debug "$1: ""${GREEN}""$2" | |
| code_folder=$2 | |
| log_debug "lc folder: ""${GREEN}""$code_folder" | |
| shift 2 | |
| ;; | |
| -t | --test_folder) | |
| log_debug "$1: ""${GREEN}""$2" | |
| test_folder=$2 | |
| log_debug "test folder: ""${GREEN}""$test_folder" | |
| shift 2 | |
| ;; | |
| -s | --scaffold) | |
| log_debug "$1: ""${GREEN}""$2" | |
| scaffold_folder_structure | |
| exit 0 | |
| ;; | |
| -u | --url) | |
| log_debug "$1: ""${GREEN}""$2" | |
| # only capture domain and file name | |
| url=$(dirname "$2")/$(basename "$2") | |
| log_debug "clean url: ""${GREEN}""$url" | |
| log_info "checking if url is valid..." | |
| if [[ ! $url =~ $urlFmtRE ]]; then | |
| log_error "invalid url" | |
| exit 1 | |
| fi | |
| log_info "${GREEN}"" url valid" | |
| shift 2 | |
| ;; | |
| -h | --help) | |
| log_usage | |
| exit 0 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| --* | -*) | |
| log_error "invalid option: ""${GREEN}""\`${1}\`" | |
| log_error "provide leetcode problem url" | |
| log_info "e.g.: ""${GREEN}""mlc.sh https://leetcode.com/problems/valid-palindrome" | |
| exit 1 | |
| ;; | |
| *) | |
| positional_args+=("$1") # save positional args | |
| shift | |
| ;; | |
| esac | |
| done | |
| # if getopt wasn't used | |
| if [ -z "$_options" ]; then | |
| # restore positional arguments | |
| set -- "${positional_args[@]}" | |
| fi | |
| # free arguments are now set as $@ | |
| # can be accessed later on | |
| log_debug "getting slug..." | |
| log_debug "${GREEN}"" ${BASH_REMATCH[1]}" | |
| slug=${BASH_REMATCH[1]} | |
| log_debug "creating curl command..." | |
| format="\u0021" | |
| # curl 'https://leetcode.com/graphql/' \ | |
| # -H 'content-type: application/json' \ | |
| # --data-raw $'{"query":"query questionTitle($titleSlug: String\u0021) {question(titleSlug: $titleSlug) { questionId questionFrontendId title titleSlug content codeSnippets {lang langSlug code} difficulty likes dislikes topicTags {slug} }}","variables":{"titleSlug":"valid-palindrome"},"operationName":"questionTitle"}' \ | |
| # --compressed | jq '.data.question' | |
| query() { | |
| cat <<EOF | |
| $'{"query":"query questionTitle(\$titleSlug: String$format) {question(titleSlug: \$titleSlug) { questionId questionFrontendId title titleSlug content codeSnippets {lang langSlug code } isPaidOnly difficulty likes dislikes topicTags {slug} }}","variables":{"titleSlug":"$slug"},"operationName":"questionTitle"}' | |
| EOF | |
| } | |
| curl_cmd() { | |
| cat <<EOF | |
| curl -s --location 'https://leetcode.com/graphql/' -H 'Content-Type: application/json' --data-raw $(query) --compressed | |
| EOF | |
| } | |
| log_debug "executing curl:" | |
| log_debug "${GREEN}"" $(curl_cmd)""${NC}" | |
| questionDetails=$(eval "$(curl_cmd)" | jq '.data.question') | |
| qid=$(echo "$questionDetails" | jq -r '.questionId') | |
| id=$(printf '%04d' "$qid") | |
| title=$(echo "$questionDetails" | jq -r '.title') | |
| difficulty=$(echo "$questionDetails" | jq -r '.difficulty' | tr "[:upper:]" "[:lower:]") | |
| topics=$(echo "$questionDetails" | jq -r '.topicTags | map(.slug) | join(", ")') | |
| # Extract content and remove HTML tags and other formatting | |
| contentRaw=$(echo "$questionDetails" | jq -r '.content') | |
| contentProcessed=$(echo "$contentRaw" | sed 's/<[^>]*>//g') # Remove HTML tags | |
| contentProcessed=$(echo "$contentProcessed" | sed 's/ / /g') # Replace non-breaking spaces with space | |
| contentProcessed=$(echo "$contentProcessed" | sed 's/</</g') # Replace less than symbol entities with actual symbol | |
| contentProcessed=$(echo "$contentProcessed" | sed 's/>/>/g') # Replace greater than symbol entities with actual symbol | |
| contentProcessed=$(echo "$contentProcessed" | sed 's/"/"/g') # Replace quote entities with actual quotes | |
| contentProcessed=$(echo "$contentProcessed" | sed 's/&/&/g') # Replace ampersand entities with actual ampersand | |
| contentProcessed=$(echo "$contentProcessed" | sed '/^$/!s/^/# /') # Add hash to non-empty lines | |
| contentProcessed=$(echo "$contentProcessed" | sed '/^$/d') # Remove extra line breaks | |
| content=$(echo "$contentProcessed") | |
| code=$(echo "$questionDetails" | jq -r '.codeSnippets[] | select(.langSlug=="python3") | .code') | |
| if [[ $difficulty = "easy" ]]; then | |
| difficultyOrdered=${difficulty_name_easy} | |
| elif [[ $difficulty = "medium" ]]; then | |
| difficultyOrdered=${difficulty_name_medium} | |
| else | |
| difficultyOrdered=${difficulty_name_hard} | |
| fi | |
| titleNoSpace=$(echo "${title//[[:blank:]]/}") | |
| log_debug "id: ""${GREEN}""$id" | |
| log_debug "title: $title -> formattedTitle: ""${GREEN}""$titleNoSpace" | |
| log_debug "difficulty: $difficulty -> orderedDifficulty: ""${GREEN}""$difficultyOrdered" | |
| log_debug "topics: ""${GREEN}""$topics" | |
| log_debug "prompt: \n""${GREEN}""$content" | |
| log_debug "code: \n${GREEN}$code" | |
| topics=$(echo "$topics" | sed -e 's;" ";, ;g' | sed -e 's;";;g') | |
| log_debug "topics: ${GREEN}$topics" | |
| filename=$(echo "$slug" | sed 's/-/_/g') | |
| file=$code_folder/$study_plan/$difficultyOrdered/${filename_prefix}${id}_${filename}.${filename_extension} | |
| methodName=$(echo $code | awk '{split($4,a,"("); print a[1]}') | |
| log_debug "methodName: ${GREEN}$methodName" | |
| methodArgs=$(echo $code | sed -E 's/.*self, //g') | |
| methodArgs=$(echo $methodArgs | sed -E 's/).*//g') | |
| methodArgs=$(echo $methodArgs | sed -E 's/: [[:alnum:]]*|[[:alnum:]]*\[[[:alnum:]]*\]//g') | |
| log_debug "methodArgs: ${GREEN}$methodArgs" | |
| baseTestParams=$(echo $methodArgs | tr -d '[:blank:]' | sed -E 's/[[:alnum:]]*/0/g' | sed -E 's/,0/, 0/g') | |
| log_debug "methodArgs: ${GREEN}$baseTestParams" | |
| log_info "writing to file: ${GREEN}$file" | |
| echo "# ${id}_${filename}.py" >"$file" | |
| echo "#" >>"$file" | |
| echo "# $title" >>"$file" | |
| echo "#" >>"$file" | |
| echo "# $url" >>"$file" | |
| echo "# topics: $topics" >>"$file" | |
| echo "#" >>"$file" | |
| echo -en "$content" >>"$file" | |
| echo -en "\n\n" >>"$file" | |
| if [[ $code == *"List["* ]]; then echo -en "from typing import List\n" >>"$file"; fi | |
| if [[ $code == *"Optional["* ]]; then echo -en "from typing import Optional\n" >>"$file"; fi | |
| echo -en "\n" >>"$file" | |
| echo -en "$code" >>"$file" | |
| echo -en "pass" >>"$file" | |
| log_info "scaffolding test $testFile: ${GREEN}${titleNoSpace}Test" | |
| testFile=$test_folder/${test_file_name} | |
| echo -en "\n\n" >>"$testFile" | |
| echo -en "import p${id}_${filename}\n" >>"$testFile" | |
| echo -en "class ${titleNoSpace}Test(unittest.TestCase):\n" >>"$testFile" | |
| echo -en " def base_test(self, $methodArgs, expected):\n" >>"$testFile" | |
| echo -en " unit_test = p${id}_${filename}.Solution()\n" >>"$testFile" | |
| echo -en " got = unit_test.$methodName($methodArgs)\n" >>"$testFile" | |
| echo -en " self.assertEqual(got, expected)\n\n" >>"$testFile" | |
| echo -en " def test_1(self):\n" >>"$testFile" | |
| echo -en " $methodArgs, expected = $baseTestParams, 0\n" >>"$testFile" | |
| echo -en " self.base_test($methodArgs, expected)\n" >>"$testFile" | |
| echo -en " pass\n" >>"$testFile" | |
| log_info "fin" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment