Skip to content

Instantly share code, notes, and snippets.

@rshmhrj
Last active July 22, 2024 17:05
Show Gist options
  • Select an option

  • Save rshmhrj/66bf43966246c7b134c64d4b55b40a95 to your computer and use it in GitHub Desktop.

Select an option

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/
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
#!/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/&nbsp;/ /g') # Replace non-breaking spaces with space
contentProcessed=$(echo "$contentProcessed" | sed 's/&lt;/</g') # Replace less than symbol entities with actual symbol
contentProcessed=$(echo "$contentProcessed" | sed 's/&gt;/>/g') # Replace greater than symbol entities with actual symbol
contentProcessed=$(echo "$contentProcessed" | sed 's/&quot;/"/g') # Replace quote entities with actual quotes
contentProcessed=$(echo "$contentProcessed" | sed 's/&amp;/&/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