Skip to content

Commit

Permalink
Merge 8767fba into 052b56b
Browse files Browse the repository at this point in the history
  • Loading branch information
fspv authored Jun 12, 2022
2 parents 052b56b + 8767fba commit 28a7f6b
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 21 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/style-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ jobs:
python-version: 3.9
- name: Install requirements
run: pip install -r requirements.txt
- name: Install test requirements
run: pip install -r test-requirements.txt
- name: Install pylint
run: pip install pylint
- name: Run pylint
run: pylint -E generate.py
run: find . -type f -name "*.py" | xargs pylint -E
black:
name: black
runs-on: ubuntu-latest
Expand Down Expand Up @@ -46,4 +48,4 @@ jobs:
- name: Install isort
run: pip install isort
- name: Run isort
run: isort --ensure-newline-before-comments --diff generate.py
run: isort --ensure-newline-before-comments --diff -v .
17 changes: 17 additions & 0 deletions .github/workflows/type-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,20 @@ jobs:
run: pip install mypy
- name: Run mypy
run: mypy .
pyre:
name: pyre
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 test requirements
run: pip install -r test-requirements.txt
- name: Install pyre
run: pip install pyre
- name: Run pyre
run: pyre check
4 changes: 3 additions & 1 deletion .pyre_configuration
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"source_directories": [
"."
]
],
"site_package_search_strategy": "all",
"strict": true
}
10 changes: 5 additions & 5 deletions generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import asyncio
import logging
from pathlib import Path
from typing import Any, Coroutine, List
from typing import Any, Awaitable, Callable, Coroutine, List

# https://github.com/kerrickstaley/genanki
import genanki # type: ignore
Expand All @@ -34,7 +34,7 @@ def parse_args() -> argparse.Namespace:
"--start", type=int, help="Start generation from this problem", default=0
)
parser.add_argument(
"--stop", type=int, help="Stop generation on this problem", default=2 ** 64
"--stop", type=int, help="Stop generation on this problem", default=2**64
)
parser.add_argument(
"--page-size",
Expand Down Expand Up @@ -64,7 +64,7 @@ class LeetcodeNote(genanki.Note):
"""

@property
def guid(self):
def guid(self) -> str:
# Hash by leetcode task handle
return genanki.guid_for(self.fields[0])

Expand Down Expand Up @@ -179,7 +179,7 @@ async def generate(
start, stop, page_size, list_id
)

note_generators: List[Coroutine[Any, Any, LeetcodeNote]] = []
note_generators: List[Awaitable[LeetcodeNote]] = []

task_handles = await leetcode_data.all_problems_handles()

Expand Down Expand Up @@ -212,5 +212,5 @@ async def main() -> None:


if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop: asyncio.events.AbstractEventLoop = asyncio.get_event_loop()
loop.run_until_complete(main())
47 changes: 36 additions & 11 deletions leetcode_anki/helpers/leetcode.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# pylint: disable=missing-module-docstring
import functools
import json
import logging
import math
import os
import time
from functools import cached_property
from typing import Callable, Dict, List, Tuple, Type
from typing import Any, Callable, Dict, List, Tuple, Type, TypeVar

# https://github.com/prius/python-leetcode
import leetcode.api.default_api # type: ignore
Expand Down Expand Up @@ -48,16 +49,28 @@ def _get_leetcode_api_client() -> leetcode.api.default_api.DefaultApi:
return api_instance


def retry(times: int, exceptions: Tuple[Type[Exception]], delay: float) -> Callable:
"""
Retry Decorator
Retries the wrapped function/method `times` times if the exceptions listed
in `exceptions` are thrown
"""
_T = TypeVar("_T")


class _RetryDecorator:
_times: int
_exceptions: Tuple[Type[Exception]]
_delay: float

def __init__(
self, times: int, exceptions: Tuple[Type[Exception]], delay: float
) -> None:
self._times = times
self._exceptions = exceptions
self._delay = delay

def __call__(self, func: Callable[..., _T]) -> Callable[..., _T]:
times: int = self._times
exceptions: Tuple[Type[Exception]] = self._exceptions
delay: float = self._delay

def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
def wrapper(*args: Any, **kwargs: Any) -> _T:
for attempt in range(times - 1):
try:
return func(*args, **kwargs)
Expand All @@ -72,7 +85,17 @@ def wrapper(*args, **kwargs):

return wrapper

return decorator

def retry(
times: int, exceptions: Tuple[Type[Exception]], delay: float
) -> _RetryDecorator:
"""
Retry Decorator
Retries the wrapped function/method `times` times if the exceptions listed
in `exceptions` are thrown
"""

return _RetryDecorator(times, exceptions, delay)


class LeetcodeData:
Expand Down Expand Up @@ -230,7 +253,7 @@ def _get_problems_data(
leetcode.models.graphql_question_detail.GraphqlQuestionDetail
] = []

logging.info(f"Fetching {stop - start + 1} problems {page_size} per page")
logging.info("Fetching %s problems %s per page", stop - start + 1, page_size)

for page in tqdm(
range(math.ceil((stop - start + 1) / page_size)),
Expand Down Expand Up @@ -261,6 +284,8 @@ def _get_problem_data(
if problem_slug in cache:
return cache[problem_slug]

raise ValueError(f"Problem {problem_slug} is not in cache")

async def _get_description(self, problem_slug: str) -> str:
"""
Problem description
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ asyncio_mode = "strict"
testpaths = [
"test",
]

[tool.pylint]
max-line-length = 88
disable = ["line-too-long"]
42 changes: 40 additions & 2 deletions test/helpers/test_leetcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,14 @@ def dummy_return_question_detail_dict(
@mock.patch("os.environ", mock.MagicMock(return_value={"LEETCODE_SESSION_ID": "test"}))
@mock.patch("leetcode.auth", mock.MagicMock())
class TestLeetcode:
# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
async def test_get_leetcode_api_client(self) -> None:
assert leetcode_anki.helpers.leetcode._get_leetcode_api_client()

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
async def test_retry(self) -> None:
decorator = leetcode_anki.helpers.leetcode.retry(
Expand Down Expand Up @@ -134,6 +138,8 @@ def setup(self) -> None:
0, 10000
)

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -142,6 +148,8 @@ def setup(self) -> None:
async def test_init(self) -> None:
self._leetcode_data._cache["test"] = QUESTION_DETAIL

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -151,6 +159,8 @@ async def test_get_description(self) -> None:
self._leetcode_data._cache["test"] = QUESTION_DETAIL
assert (await self._leetcode_data.description("test")) == "test content"

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -162,6 +172,8 @@ async def test_submissions(self) -> None:
assert (await self._leetcode_data.submissions_total("test")) == 1
assert (await self._leetcode_data.submissions_accepted("test")) == 1

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -173,6 +185,8 @@ async def test_difficulty_easy(self) -> None:
QUESTION_DETAIL.difficulty = "Easy"
assert "Easy" in (await self._leetcode_data.difficulty("test"))

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -184,6 +198,8 @@ async def test_difficulty_medium(self) -> None:
QUESTION_DETAIL.difficulty = "Medium"
assert "Medium" in (await self._leetcode_data.difficulty("test"))

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -195,6 +211,8 @@ async def test_difficulty_hard(self) -> None:
QUESTION_DETAIL.difficulty = "Hard"
assert "Hard" in (await self._leetcode_data.difficulty("test"))

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -205,6 +223,8 @@ async def test_paid(self) -> None:

assert (await self._leetcode_data.paid("test")) is False

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -215,6 +235,8 @@ async def test_problem_id(self) -> None:

assert (await self._leetcode_data.problem_id("test")) == "1"

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -225,6 +247,8 @@ async def test_likes(self) -> None:

assert (await self._leetcode_data.likes("test")) == 1

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -235,6 +259,8 @@ async def test_dislikes(self) -> None:

assert (await self._leetcode_data.dislikes("test")) == 1

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -248,6 +274,8 @@ async def test_tags(self) -> None:
"difficulty-hard-tag",
]

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -258,6 +286,8 @@ async def test_freq_bar(self) -> None:

assert (await self._leetcode_data.freq_bar("test")) == 1.1

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data",
Expand All @@ -267,6 +297,8 @@ async def test_get_problem_data(self) -> None:
assert self._leetcode_data._cache["test"] == QUESTION_DETAIL

@mock.patch("time.sleep", mock.Mock())
# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
async def test_get_problems_data_page(self) -> None:
data = leetcode.models.graphql_data.GraphqlData(
Expand All @@ -281,14 +313,20 @@ async def test_get_problems_data_page(self) -> None:
QUESTION_DETAIL
]

# pyre-fixme[56]: Pyre was not able to infer the type of the decorator
# `pytest.mark.asyncio`.
@pytest.mark.asyncio
@mock.patch(
"leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_count",
mock.Mock(return_value=234),
)
@mock.patch("leetcode_anki.helpers.leetcode.LeetcodeData._get_problems_data_page")
async def test_get_problems_data(self, mock_get_problems_data_page) -> None:
question_list = [QUESTION_DETAIL] * 234
async def test_get_problems_data(
self, mock_get_problems_data_page: mock.Mock
) -> None:
question_list: List[
leetcode.models.graphql_question_detail.GraphqlQuestionDetail
] = [QUESTION_DETAIL] * 234

def dummy(
offset: int, page_size: int, page: int
Expand Down

0 comments on commit 28a7f6b

Please sign in to comment.