Skip to content

Added QA workflows and made the code compliant #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/style-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: Style
on: push
jobs:
pylint:
name: pylint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install pylint
run: pip install pylint
- name: Run pylint
run: pylint -E generate.py
black:
name: black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install black
run: pip install black
- name: Run black
run: black --diff .
isort:
name: isort
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install isort
run: pip install isort
- name: Run isort
run: isort --ensure-newline-before-comments --diff generate.py
19 changes: 19 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Tests
on: push
jobs:
pytest:
name: pytest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install pytest
run: pip install pytest
- name: Run pytest
run: pytest
19 changes: 19 additions & 0 deletions .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Typing
on: push
jobs:
mypy:
name: mypy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install mypy
run: pip install mypy
- name: Run mypy
run: mypy .
114 changes: 98 additions & 16 deletions generate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#!/usr/bin/env python3
"""
This script generates an Anki deck with all the leetcode problems currently
known.
"""

import argparse
import asyncio
import functools
Expand All @@ -9,14 +14,16 @@
from functools import lru_cache
from typing import Any, Callable, Coroutine, Dict, Iterator, List, Tuple

import diskcache
import diskcache # type: ignore

# https://github.com/kerrickstaley/genanki
import genanki # type: ignore

# https://github.com/prius/python-leetcode
import leetcode # type: ignore
import leetcode.auth # type: ignore
import urllib3
from tqdm import tqdm
import urllib3 # type: ignore
from tqdm import tqdm # type: ignore

LEETCODE_ANKI_MODEL_ID = 4567610856
LEETCODE_ANKI_DECK_ID = 8589798175
Expand All @@ -31,6 +38,9 @@


def parse_args() -> argparse.Namespace:
"""
Parse command line arguments for the script
"""
parser = argparse.ArgumentParser(description="Generate Anki cards for leetcode")
parser.add_argument(
"--start", type=int, help="Start generation from this problem", default=0
Expand Down Expand Up @@ -58,7 +68,9 @@ async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except exceptions:
logging.exception(f"Exception occured, try {attempt + 1}/{times}")
logging.exception(
"Exception occured, try %s/%s", attempt + 1, times
)
time.sleep(delay)

logging.error("Last try")
Expand All @@ -70,18 +82,29 @@ async def wrapper(*args, **kwargs):


class LeetcodeData:
def __init__(self) -> None:
"""
Retrieves and caches the data for problems, acquired from the leetcode API.

# Initialize leetcode API client
This data can be later accessed using provided methods with corresponding
names.
"""

def __init__(self) -> None:
"""
Initialize leetcode API and disk cache for API responses
"""
self._api_instance = get_leetcode_api_client()

# Init problem data cache
if not os.path.exists(CACHE_DIR):
os.mkdir(CACHE_DIR)
self._cache = diskcache.Cache(CACHE_DIR)

@retry(times=3, exceptions=(urllib3.exceptions.ProtocolError,), delay=5)
async def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
"""
Get data about a specific problem (method output if cached to reduce
the load on the leetcode API)
"""
if problem_slug in self._cache:
return self._cache[problem_slug]

Expand Down Expand Up @@ -161,47 +184,74 @@ async def _get_problem_data(self, problem_slug: str) -> Dict[str, str]:
return data

async def _get_description(self, problem_slug: str) -> str:
"""
Problem description
"""
data = await self._get_problem_data(problem_slug)
return data.content or "No content"

async def _stats(self, problem_slug: str) -> Dict[str, str]:
"""
Various stats about problem. Such as number of accepted solutions, etc.
"""
data = await self._get_problem_data(problem_slug)
return json.loads(data.stats)

async def submissions_total(self, problem_slug: str) -> int:
return (await self._stats(problem_slug))["totalSubmissionRaw"]
"""
Total number of submissions of the problem
"""
return int((await self._stats(problem_slug))["totalSubmissionRaw"])

async def submissions_accepted(self, problem_slug: str) -> int:
return (await self._stats(problem_slug))["totalAcceptedRaw"]
"""
Number of accepted submissions of the problem
"""
return int((await self._stats(problem_slug))["totalAcceptedRaw"])

async def description(self, problem_slug: str) -> str:
"""
Problem description
"""
return await self._get_description(problem_slug)

async def solution(self, problem_slug: str) -> str:
return ""

async def difficulty(self, problem_slug: str) -> str:
"""
Problem difficulty. Returns colored HTML version, so it can be used
directly in Anki
"""
data = await self._get_problem_data(problem_slug)
diff = data.difficulty

if diff == "Easy":
return "<font color='green'>Easy</font>"
elif diff == "Medium":

if diff == "Medium":
return "<font color='orange'>Medium</font>"
elif diff == "Hard":

if diff == "Hard":
return "<font color='red'>Hard</font>"
else:
raise ValueError(f"Incorrect difficulty: {diff}")

raise ValueError(f"Incorrect difficulty: {diff}")

async def paid(self, problem_slug: str) -> str:
"""
Problem's "available for paid subsribers" status
"""
data = await self._get_problem_data(problem_slug)
return data.is_paid_only

async def problem_id(self, problem_slug: str) -> str:
"""
Numerical id of the problem
"""
data = await self._get_problem_data(problem_slug)
return data.question_frontend_id

async def likes(self, problem_slug: str) -> int:
"""
Number of likes for the problem
"""
data = await self._get_problem_data(problem_slug)
likes = data.likes

Expand All @@ -211,6 +261,9 @@ async def likes(self, problem_slug: str) -> int:
return likes

async def dislikes(self, problem_slug: str) -> int:
"""
Number of dislikes for the problem
"""
data = await self._get_problem_data(problem_slug)
dislikes = data.dislikes

Expand All @@ -220,15 +273,26 @@ async def dislikes(self, problem_slug: str) -> int:
return dislikes

async def tags(self, problem_slug: str) -> List[str]:
"""
List of the tags for this problem (string slugs)
"""
data = await self._get_problem_data(problem_slug)
return list(map(lambda x: x.slug, data.topic_tags))

async def freq_bar(self, problem_slug: str) -> float:
"""
Returns percentage for frequency bar
"""
data = await self._get_problem_data(problem_slug)
return data.freq_bar or 0


class LeetcodeNote(genanki.Note):
"""
Extended base class for the Anki note, that correctly sets the unique
identifier of the note.
"""

@property
def guid(self):
# Hash by leetcode task handle
Expand All @@ -237,6 +301,12 @@ def guid(self):

@lru_cache(None)
def get_leetcode_api_client() -> leetcode.DefaultApi:
"""
Leetcode API instance constructor.

This is a singleton, because we don't need to create a separate client
each time
"""
configuration = leetcode.Configuration()

session_id = os.environ["LEETCODE_SESSION_ID"]
Expand All @@ -253,6 +323,9 @@ def get_leetcode_api_client() -> leetcode.DefaultApi:


def get_leetcode_task_handles() -> Iterator[Tuple[str, str, str]]:
"""
Get task handles for all the leetcode problems.
"""
api_instance = get_leetcode_api_client()

for topic in ["algorithms", "database", "shell", "concurrency"]:
Expand All @@ -270,6 +343,9 @@ async def generate_anki_note(
leetcode_task_title: str,
topic: str,
) -> LeetcodeNote:
"""
Generate a single Anki flashcard
"""
return LeetcodeNote(
model=leetcode_model,
fields=[
Expand Down Expand Up @@ -300,6 +376,9 @@ async def generate_anki_note(


async def generate(start: int, stop: int) -> None:
"""
Generate an Anki deck
"""
leetcode_model = genanki.Model(
LEETCODE_ANKI_MODEL_ID,
"Leetcode model",
Expand Down Expand Up @@ -386,6 +465,9 @@ async def generate(start: int, stop: int) -> None:


async def main() -> None:
"""
The main script logic
"""
args = parse_args()

start, stop = args.start, args.stop
Expand Down
Binary file not shown.
19 changes: 19 additions & 0 deletions test/test_dummy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Just a placeholder
"""


class TestDummy:
"""
Dummy test
"""

@staticmethod
def test_do_nothing() -> None:
"""Do nothing"""
assert True

@staticmethod
def test_do_nothing2() -> None:
"""Do nothing"""
assert True