Skip to content

Commit 11583f0

Browse files
committed
commit first working version
0 parents  commit 11583f0

File tree

12 files changed

+365
-0
lines changed

12 files changed

+365
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__
2+
dist
3+
*.egg-info

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# LeetCode Project Generator
2+
3+
Copyright (c) 2023 Konrad Guzek
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# LeetCode Project Generator
2+
3+
A CLI tool created using [Click](https://click.palletsprojects.com/en/8.1.x/).
4+
This utility creates a barebones project template from the URL of a LeetCode problem.
5+
6+
## Usage
7+
8+
Ensure the package is recognised on PYTHONPATH, and invoke using the command:
9+
10+
```sh
11+
python lpg.py (--title_slug <problem title> | --url <problem url>) [--directory <project directory>] [--lang <language>] [--force] [--git-init]
12+
```
13+
14+
"Title slug" refers to the dashed title of the LeetCode problem which can be found in the URL of the problem.
15+
E.g. for [https://leetcode.com/problems/two-sum/](https://leetcode.com/problems/two-sum/), the title slug is `two-sum`.
16+
17+
The default language is C. Other languages are currently unsupported.
18+
If using Bash, you must surround the URL with quotes, since the `&` symbol would be interpreted as an asynchronous command.
19+
20+
The project directory defaults to `~/Documents/Coding/{language_name}/`. You may use use the template `{language_name}` when specifying the directory, and this will automatically be translated into the name of the language specified using `--lang`. E.g.: `cpp -> C++`.
21+
The project will be created in its own directory with the name of the title slug within this directory. This is unchangeable.
22+
23+
For more syntax help, use the `help` option.
24+
25+
```sh
26+
python lpg.py --help
27+
```
28+
29+
Planned available languages:
30+
31+
- python
32+
- python3
33+
- javascript
34+
- typescript
35+
- c
36+
- cpp
37+
- csharp
38+
- java
39+
- php
40+
- swift
41+
- kotlin
42+
- dart
43+
- golang
44+
- ruby
45+
- scala
46+
- rust
47+
- racket
48+
- erlang
49+
- elixir
50+
51+
Thanks for reading!

pyproject.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "lpg"
7+
version = "1.0.0"
8+
authors = [{ name = "Konrad Guzek", email = "konrad@guzek.uk" }]
9+
description = "A LeetCode project template generator."
10+
readme = "README.md"
11+
requires-python = ">=3.10"
12+
classifiers = [
13+
"Programming Language :: Python :: 3",
14+
"License :: OSI Approved :: MIT License",
15+
"Operating System :: OS Independent",
16+
]
17+
18+
[project.urls]
19+
Homepage = "https://github.com/kguzek/leetcode-project-generator"
20+
Issues = "https://github.com/kguzek/leetcode-project-generator/issues"

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
click

setup.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import setuptools
2+
3+
module_name = "lpg"
4+
setuptools.setup(
5+
name=module_name,
6+
py_modules=[module_name],
7+
)

src/lpg/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package definition for the LeetCode Project Generator."""

src/lpg/interfaces/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Package definition for various APIs."""

src/lpg/interfaces/file.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""API for accessing various OS and filesystem functions."""
2+
3+
4+
import os
5+
from click import ClickException
6+
7+
from .lang import c
8+
9+
10+
def create_project_directory(project_path: str, force: bool):
11+
"""Creates the project directory. If it already exists, throws a `ClickException`."""
12+
try:
13+
os.makedirs(project_path, exist_ok=force)
14+
except FileExistsError as error:
15+
raise ClickException(
16+
f"Cannot create project with path '{project_path}' as it already exists."
17+
) from error
18+
except OSError as error:
19+
raise ClickException(f"Invalid project path '{project_path}'") from error
20+
return project_path
21+
22+
23+
def create_project(
24+
project_name: str, project_directory: str, template_data: dict, force: bool
25+
):
26+
"""Creates the entire project."""
27+
28+
language_code = template_data["langSlug"]
29+
language_name = template_data["lang"]
30+
template = template_data["code"]
31+
32+
project_path = os.path.expanduser(
33+
os.path.join(
34+
project_directory.format(language_name=language_name), project_name
35+
)
36+
)
37+
create_project_directory(project_path, force)
38+
os.chdir(project_path)
39+
40+
match language_code:
41+
case "c":
42+
c.create_c_project(template)
43+
case "*":
44+
raise ClickException(f"{language_name} projects are currently unsupported.")

src/lpg/interfaces/lang/c.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Project generator for the C language."""
2+
3+
4+
import re
5+
6+
HEADER_FILE_TEMPLATE = "{returnType} {name}({params});"
7+
8+
TEST_FILE_TEMPLATE = """\
9+
#include <stdio.h>
10+
11+
int main() {{
12+
{param_declarations};
13+
{returnType} result = {name}({params_call});
14+
printf("result: %d\n", result);
15+
return 0;
16+
}}
17+
"""
18+
19+
FUNCTION_SIGNATURE_PATTERN = re.compile(
20+
r"(?P<returnType>\w+(?:\[\]|\*)?) (?P<name>\w+)\((?P<params>(?:\w+(?:\[\]|\*)? \w+(?:, )?)+)\) {"
21+
)
22+
23+
24+
def create_c_project(template: str):
25+
"""Creates the project template for C."""
26+
27+
match = FUNCTION_SIGNATURE_PATTERN.search(template, re.MULTILINE)
28+
if match is None:
29+
raise RuntimeError("Fatal error: project template doesn't match regex.")
30+
groups = match.groupdict()
31+
32+
with open("solution.c", "w", encoding="utf-8") as file:
33+
file.write(template + "\n")
34+
35+
with open("solution.h", "w", encoding="utf-8") as file:
36+
file.write(HEADER_FILE_TEMPLATE.format(**groups))
37+
38+
params = groups["params"].split(", ")
39+
groups["param_declarations"] = groups["params"].replace(", ", ";\n ")
40+
groups["params_call"] = ", ".join(param.split()[1] for param in params)
41+
formatted = TEST_FILE_TEMPLATE.format(**groups)
42+
43+
with open("test.c", "w", encoding="utf-8") as file:
44+
file.write(formatted)

src/lpg/interfaces/web.py

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""The API for accessing LeetCode's front end."""
2+
3+
import json
4+
import re
5+
import urllib.error
6+
import urllib.parse
7+
import urllib.request
8+
from http.client import HTTPResponse
9+
10+
from click import ClickException
11+
12+
URL_TITLE_SLUG_PATTERN = re.compile(
13+
r"^https://leetcode\.com/problems/(?P<slug>[-\w]+)/"
14+
)
15+
TITLE_SLUG_PATTERN = re.compile(r"\w+(?:-\w+)+")
16+
17+
LEETCODE_API_URL = "https://leetcode.com/graphql/"
18+
GRAPHQL_QUERY_TEMPLATE = """
19+
query questionEditorData($titleSlug: String!) {
20+
question(titleSlug: $titleSlug) {
21+
# questionId
22+
# questionFrontendId
23+
codeSnippets {
24+
lang
25+
langSlug
26+
code
27+
}
28+
# envInfo
29+
# enableRunCode
30+
}
31+
}
32+
"""
33+
FAKE_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11"
34+
35+
36+
def _validate_title_slug(title_slug: str) -> str:
37+
match = TITLE_SLUG_PATTERN.fullmatch(title_slug)
38+
if match is None:
39+
raise ClickException(f"Invalid title slug '{title_slug}'.")
40+
41+
42+
def _get_title_slug(problem_url: str) -> str:
43+
match = URL_TITLE_SLUG_PATTERN.match(problem_url)
44+
if match is None:
45+
raise ClickException(f"Invalid LeetCode problem URL '{problem_url}'.")
46+
title_slug = match.group("slug")
47+
return title_slug
48+
49+
50+
def _create_graphql_request(title_slug: str):
51+
"""Creates a GraphQL query to fetch the problem code using the title slug."""
52+
body = {"query": GRAPHQL_QUERY_TEMPLATE, "variables": {"titleSlug": title_slug}}
53+
body_bytes = bytes(json.dumps(body), encoding="utf-8")
54+
headers = {"User-Agent": FAKE_USER_AGENT, "Content-Type": "application/json"}
55+
request = urllib.request.Request(
56+
LEETCODE_API_URL, body_bytes, method="POST", headers=headers
57+
)
58+
return request
59+
60+
61+
def _get_body_from_request(request: urllib.request.Request) -> dict:
62+
try:
63+
response: HTTPResponse = urllib.request.urlopen(request)
64+
except urllib.error.URLError as error:
65+
print(str(error))
66+
raise ClickException(f"Could not get data from API.") from error
67+
text = response.read()
68+
body: str = text.decode("utf-8")
69+
data = json.loads(body)
70+
return data
71+
72+
73+
def _get_template_data_from_body(body: dict, lang: str) -> dict[str, any]:
74+
question = body["data"]["question"]
75+
try:
76+
code_snippets = question["codeSnippets"]
77+
except TypeError:
78+
raise ClickException("Invalid title slug.")
79+
for lang_data in code_snippets:
80+
if lang_data["langSlug"] != lang:
81+
continue
82+
return lang_data
83+
else:
84+
raise ClickException(f"Invalid code language '{lang}'.")
85+
86+
87+
def get_leetcode_template(title_slug: str, url: str, lang: str):
88+
if url is None:
89+
if title_slug is None:
90+
raise ClickException("Either url or title slug must be specified.")
91+
else:
92+
title_slug = _get_title_slug(url)
93+
request = _create_graphql_request(title_slug)
94+
body = _get_body_from_request(request)
95+
template_data = _get_template_data_from_body(body, lang)
96+
with open("a.json", "w", encoding="utf-8") as file:
97+
json.dump(template_data, file)
98+
return title_slug, template_data

src/lpg/lpg.py

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
LeetCode Project Generator
3+
A program that automatically generates a C project template given the LeetCode problem URL.
4+
5+
Author: Konrad Guzek
6+
"""
7+
8+
import subprocess
9+
10+
import click
11+
12+
from interfaces import web as web_interface
13+
from interfaces import file as file_interface
14+
15+
# TODO: Update
16+
SUPPORTED_LANGUAGES = ["c"]
17+
18+
19+
@click.command()
20+
@click.option(
21+
"--title-slug",
22+
"-s",
23+
help="The dash-separated name of the problem as it appears in the URL.",
24+
)
25+
@click.option(
26+
"--url",
27+
"-u",
28+
help="The URL to the LeetCode problem webpage.",
29+
)
30+
@click.option(
31+
"--lang",
32+
"-l",
33+
help="The language of the code to generate.",
34+
default="c",
35+
)
36+
@click.option(
37+
"--directory",
38+
"-d",
39+
help="The directory for the project to be created in.",
40+
default=R"~/Documents/Coding/{language_name}/leetcode/",
41+
)
42+
@click.option(
43+
"--force",
44+
"-f",
45+
help="Force-creates the project directory even if it already exists.",
46+
default=False,
47+
is_flag=True,
48+
show_default=True,
49+
)
50+
@click.option(
51+
"--git-init",
52+
"-g",
53+
help="Initialises a git repository in the project directory.",
54+
default=False,
55+
is_flag=True,
56+
show_default=False,
57+
)
58+
def lpg(
59+
title_slug: str, url: str, lang: str, directory: str, force: bool, git_init: bool
60+
):
61+
"""CLI Entry point."""
62+
if lang not in SUPPORTED_LANGUAGES:
63+
raise click.ClickException(f"Unsupported language {lang}.")
64+
title_slug, template_data = web_interface.get_leetcode_template(
65+
title_slug, url, lang
66+
)
67+
title_slug = "least-sum"
68+
file_interface.create_project(title_slug, directory, template_data, force)
69+
if git_init:
70+
subprocess.run("git init")
71+
72+
73+
if __name__ == "__main__":
74+
lpg()

0 commit comments

Comments
 (0)