diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f0ebacc..d76679c 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -13,6 +13,8 @@ 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 pytest
run: pip install pytest
- name: Run pytest
diff --git a/.github/workflows/type-check.yml b/.github/workflows/type-check.yml
index 88cbf3a..139f1c1 100644
--- a/.github/workflows/type-check.yml
+++ b/.github/workflows/type-check.yml
@@ -13,6 +13,8 @@ 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 mypy
run: pip install mypy
- name: Run mypy
diff --git a/generate.py b/generate.py
index 41978ec..240c82b 100755
--- a/generate.py
+++ b/generate.py
@@ -6,37 +6,20 @@
import argparse
import asyncio
-import functools
-import json
import logging
-import os
-import time
-from functools import lru_cache
-from typing import Any, Callable, Coroutine, Dict, Iterator, List, Tuple
-
-import diskcache # type: ignore
+from typing import Any, Coroutine, List
# https://github.com/kerrickstaley/genanki
import genanki # type: ignore
-
-# https://github.com/prius/python-leetcode
-import leetcode.api.default_api # type: ignore
-import leetcode.api_client # type: ignore
-import leetcode.auth # type: ignore
-import leetcode.configuration # type: ignore
-import leetcode.models.graphql_query # type: ignore
-import leetcode.models.graphql_query_get_question_detail_variables # type: ignore
-import urllib3 # type: ignore
from tqdm import tqdm # type: ignore
+import leetcode_anki.helpers.leetcode
+
LEETCODE_ANKI_MODEL_ID = 4567610856
LEETCODE_ANKI_DECK_ID = 8589798175
OUTPUT_FILE = "leetcode.apkg"
-CACHE_DIR = "cache"
ALLOWED_EXTENSIONS = {".py", ".go"}
-leetcode_api_access_lock = asyncio.Lock()
-
logging.getLogger().setLevel(logging.INFO)
@@ -58,241 +41,6 @@ def parse_args() -> argparse.Namespace:
return args
-def retry(times: int, exceptions: Tuple[Exception], delay: float) -> Callable:
- """
- Retry Decorator
- Retries the wrapped function/method `times` times if the exceptions listed
- in `exceptions` are thrown
- """
-
- def decorator(func):
- @functools.wraps(func)
- async def wrapper(*args, **kwargs):
- for attempt in range(times - 1):
- try:
- return await func(*args, **kwargs)
- except exceptions:
- logging.exception(
- "Exception occured, try %s/%s", attempt + 1, times
- )
- time.sleep(delay)
-
- logging.error("Last try")
- return await func(*args, **kwargs)
-
- return wrapper
-
- return decorator
-
-
-class LeetcodeData:
- """
- Retrieves and caches the data for problems, acquired from the leetcode API.
-
- 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()
-
- 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]
-
- api_instance = self._api_instance
-
- graphql_request = leetcode.models.graphql_query.GraphqlQuery(
- query="""
- query getQuestionDetail($titleSlug: String!) {
- question(titleSlug: $titleSlug) {
- freqBar
- questionId
- questionFrontendId
- boundTopicId
- title
- content
- translatedTitle
- translatedContent
- isPaidOnly
- difficulty
- likes
- dislikes
- isLiked
- similarQuestions
- contributors {
- username
- profileUrl
- avatarUrl
- __typename
- }
- langToValidPlayground
- topicTags {
- name
- slug
- translatedName
- __typename
- }
- companyTagStats
- codeSnippets {
- lang
- langSlug
- code
- __typename
- }
- stats
- hints
- solution {
- id
- canSeeDetail
- __typename
- }
- status
- sampleTestCase
- metaData
- judgerAvailable
- judgeType
- mysqlSchemas
- enableRunCode
- enableTestMode
- envInfo
- __typename
- }
- }
- """,
- variables=leetcode.models.graphql_query_get_question_detail_variables.GraphqlQueryGetQuestionDetailVariables( # noqa: E501
- title_slug=problem_slug
- ),
- operation_name="getQuestionDetail",
- )
-
- # Critical section. Don't allow more than one parallel request to
- # the Leetcode API
- async with leetcode_api_access_lock:
- time.sleep(2) # Leetcode has a rate limiter
- data = api_instance.graphql_post(body=graphql_request).data.question
-
- # Save data in the cache
- self._cache[problem_slug] = data
-
- 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:
- """
- Total number of submissions of the problem
- """
- return int((await self._stats(problem_slug))["totalSubmissionRaw"])
-
- async def submissions_accepted(self, problem_slug: str) -> int:
- """
- 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 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 "Easy"
-
- if diff == "Medium":
- return "Medium"
-
- if diff == "Hard":
- return "Hard"
-
- 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
-
- if not isinstance(likes, int):
- raise ValueError(f"Likes should be int: {likes}")
-
- 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
-
- if not isinstance(dislikes, int):
- raise ValueError(f"Dislikes should be int: {dislikes}")
-
- 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
@@ -305,47 +53,8 @@ def guid(self):
return genanki.guid_for(self.fields[0])
-@lru_cache(None)
-def get_leetcode_api_client() -> leetcode.api.default_api.DefaultApi:
- """
- Leetcode API instance constructor.
-
- This is a singleton, because we don't need to create a separate client
- each time
- """
- configuration = leetcode.configuration.Configuration()
-
- session_id = os.environ["LEETCODE_SESSION_ID"]
- csrf_token = leetcode.auth.get_csrf_cookie(session_id)
-
- configuration.api_key["x-csrftoken"] = csrf_token
- configuration.api_key["csrftoken"] = csrf_token
- configuration.api_key["LEETCODE_SESSION"] = session_id
- configuration.api_key["Referer"] = "https://leetcode.com"
- configuration.debug = False
- api_instance = leetcode.api.default_api.DefaultApi(
- leetcode.api_client.ApiClient(configuration)
- )
-
- return api_instance
-
-
-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"]:
- api_response = api_instance.api_problems_topic_get(topic=topic)
- for stat_status_pair in api_response.stat_status_pairs:
- stat = stat_status_pair.stat
-
- yield (topic, stat.question__title, stat.question__title_slug)
-
-
async def generate_anki_note(
- leetcode_data: LeetcodeData,
+ leetcode_data: leetcode_anki.helpers.leetcode.LeetcodeData,
leetcode_model: genanki.Model,
leetcode_task_handle: str,
leetcode_task_title: str,
@@ -449,12 +158,12 @@ async def generate(start: int, stop: int) -> None:
],
)
leetcode_deck = genanki.Deck(LEETCODE_ANKI_DECK_ID, "leetcode")
- leetcode_data = LeetcodeData()
+ leetcode_data = leetcode_anki.helpers.leetcode.LeetcodeData()
note_generators: List[Coroutine[Any, Any, LeetcodeNote]] = []
for topic, leetcode_task_title, leetcode_task_handle in list(
- get_leetcode_task_handles()
+ leetcode_anki.helpers.leetcode.get_leetcode_task_handles()
)[start:stop]:
note_generators.append(
generate_anki_note(
diff --git a/leetcode_anki/__init__.py b/leetcode_anki/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode_anki/helpers/__init__.py b/leetcode_anki/helpers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/leetcode_anki/helpers/leetcode.py b/leetcode_anki/helpers/leetcode.py
new file mode 100644
index 0000000..a416f98
--- /dev/null
+++ b/leetcode_anki/helpers/leetcode.py
@@ -0,0 +1,298 @@
+import asyncio
+import functools
+import json
+import logging
+import os
+import time
+from functools import lru_cache
+from typing import Callable, Dict, Iterator, List, Tuple, Type
+
+import diskcache # type: ignore
+
+# https://github.com/prius/python-leetcode
+import leetcode.api.default_api # type: ignore
+import leetcode.api_client # type: ignore
+import leetcode.auth # type: ignore
+import leetcode.configuration # type: ignore
+import leetcode.models.graphql_query # type: ignore
+import leetcode.models.graphql_query_get_question_detail_variables # type: ignore
+import urllib3 # type: ignore
+
+CACHE_DIR = "cache"
+
+
+leetcode_api_access_lock = asyncio.Lock()
+
+
+@lru_cache(None)
+def _get_leetcode_api_client() -> leetcode.api.default_api.DefaultApi:
+ """
+ Leetcode API instance constructor.
+
+ This is a singleton, because we don't need to create a separate client
+ each time
+ """
+ configuration = leetcode.configuration.Configuration()
+
+ session_id = os.environ["LEETCODE_SESSION_ID"]
+ csrf_token = leetcode.auth.get_csrf_cookie(session_id)
+
+ configuration.api_key["x-csrftoken"] = csrf_token
+ configuration.api_key["csrftoken"] = csrf_token
+ configuration.api_key["LEETCODE_SESSION"] = session_id
+ configuration.api_key["Referer"] = "https://leetcode.com"
+ configuration.debug = False
+ api_instance = leetcode.api.default_api.DefaultApi(
+ leetcode.api_client.ApiClient(configuration)
+ )
+
+ return api_instance
+
+
+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"]:
+ api_response = api_instance.api_problems_topic_get(topic=topic)
+ for stat_status_pair in api_response.stat_status_pairs:
+ stat = stat_status_pair.stat
+
+ yield (topic, stat.question__title, stat.question__title_slug)
+
+
+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
+ """
+
+ def decorator(func):
+ @functools.wraps(func)
+ async def wrapper(*args, **kwargs):
+ for attempt in range(times - 1):
+ try:
+ return await func(*args, **kwargs)
+ except exceptions:
+ logging.exception(
+ "Exception occured, try %s/%s", attempt + 1, times
+ )
+ time.sleep(delay)
+
+ logging.error("Last try")
+ return await func(*args, **kwargs)
+
+ return wrapper
+
+ return decorator
+
+
+class LeetcodeData:
+ """
+ Retrieves and caches the data for problems, acquired from the leetcode API.
+
+ 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()
+
+ 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]
+
+ api_instance = self._api_instance
+
+ graphql_request = leetcode.models.graphql_query.GraphqlQuery(
+ query="""
+ query getQuestionDetail($titleSlug: String!) {
+ question(titleSlug: $titleSlug) {
+ freqBar
+ questionId
+ questionFrontendId
+ boundTopicId
+ title
+ content
+ translatedTitle
+ translatedContent
+ isPaidOnly
+ difficulty
+ likes
+ dislikes
+ isLiked
+ similarQuestions
+ contributors {
+ username
+ profileUrl
+ avatarUrl
+ __typename
+ }
+ langToValidPlayground
+ topicTags {
+ name
+ slug
+ translatedName
+ __typename
+ }
+ companyTagStats
+ codeSnippets {
+ lang
+ langSlug
+ code
+ __typename
+ }
+ stats
+ hints
+ solution {
+ id
+ canSeeDetail
+ __typename
+ }
+ status
+ sampleTestCase
+ metaData
+ judgerAvailable
+ judgeType
+ mysqlSchemas
+ enableRunCode
+ enableTestMode
+ envInfo
+ __typename
+ }
+ }
+ """,
+ variables=leetcode.models.graphql_query_get_question_detail_variables.GraphqlQueryGetQuestionDetailVariables( # noqa: E501
+ title_slug=problem_slug
+ ),
+ operation_name="getQuestionDetail",
+ )
+
+ # Critical section. Don't allow more than one parallel request to
+ # the Leetcode API
+ async with leetcode_api_access_lock:
+ time.sleep(2) # Leetcode has a rate limiter
+ data = api_instance.graphql_post(body=graphql_request).data.question
+
+ # Save data in the cache
+ self._cache[problem_slug] = data
+
+ 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:
+ """
+ Total number of submissions of the problem
+ """
+ return int((await self._stats(problem_slug))["totalSubmissionRaw"])
+
+ async def submissions_accepted(self, problem_slug: str) -> int:
+ """
+ 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 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 "Easy"
+
+ if diff == "Medium":
+ return "Medium"
+
+ if diff == "Hard":
+ return "Hard"
+
+ 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
+
+ if not isinstance(likes, int):
+ raise ValueError(f"Likes should be int: {likes}")
+
+ 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
+
+ if not isinstance(dislikes, int):
+ raise ValueError(f"Dislikes should be int: {dislikes}")
+
+ 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
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..e079f8a
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1 @@
+pytest
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/__init__.py b/test/helpers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/helpers/test_leetcode.py b/test/helpers/test_leetcode.py
new file mode 100644
index 0000000..a0e7e46
--- /dev/null
+++ b/test/helpers/test_leetcode.py
@@ -0,0 +1,242 @@
+import sys
+from typing import Optional
+from unittest import mock
+
+import leetcode.models.graphql_data # type: ignore
+import leetcode.models.graphql_question_contributor # type: ignore
+import leetcode.models.graphql_question_detail # type: ignore
+import leetcode.models.graphql_question_solution # type: ignore
+import leetcode.models.graphql_question_topic_tag # type: ignore
+import leetcode.models.graphql_response # type: ignore
+import leetcode.models.problems # type: ignore
+import leetcode.models.stat # type: ignore
+import leetcode.models.stat_status_pair # type: ignore
+import pytest
+
+import leetcode_anki.helpers.leetcode
+
+
+@mock.patch("os.environ", mock.MagicMock(return_value={"LEETCODE_SESSION_ID": "test"}))
+@mock.patch("leetcode.auth", mock.MagicMock())
+class TestLeetcode:
+ @pytest.mark.asyncio
+ async def test_get_leetcode_api_client(self) -> None:
+ assert leetcode_anki.helpers.leetcode._get_leetcode_api_client()
+
+ @pytest.mark.asyncio
+ @mock.patch("leetcode_anki.helpers.leetcode._get_leetcode_api_client")
+ async def test_get_leetcode_task_handles(self, api_client: mock.Mock) -> None:
+ problems = leetcode.models.problems.Problems(
+ user_name="test",
+ num_solved=1,
+ num_total=1,
+ ac_easy=1,
+ ac_medium=1,
+ ac_hard=1,
+ frequency_high=1,
+ frequency_mid=1,
+ category_slug="test",
+ stat_status_pairs=[
+ leetcode.models.stat_status_pair.StatStatusPair(
+ stat=leetcode.models.stat.Stat(
+ question_id=1,
+ question__hide=False,
+ question__title="Test 1",
+ question__title_slug="test1",
+ is_new_question=False,
+ frontend_question_id=1,
+ total_acs=1,
+ total_submitted=1,
+ ),
+ difficulty="easy",
+ is_favor=False,
+ status="ac",
+ paid_only=False,
+ frequency=0.0,
+ progress=1,
+ ),
+ ],
+ )
+ api_client.return_value.api_problems_topic_get.return_value = problems
+
+ assert list(leetcode_anki.helpers.leetcode.get_leetcode_task_handles()) == [
+ ("algorithms", "Test 1", "test1"),
+ ("database", "Test 1", "test1"),
+ ("shell", "Test 1", "test1"),
+ ("concurrency", "Test 1", "test1"),
+ ]
+
+ @pytest.mark.asyncio
+ async def test_retry(self) -> None:
+ decorator = leetcode_anki.helpers.leetcode.retry(
+ times=3, exceptions=(RuntimeError,), delay=0.01
+ )
+
+ async def test() -> str:
+ return "test"
+
+ func = mock.Mock(side_effect=[RuntimeError, RuntimeError, test()])
+
+ wrapper = decorator(func)
+
+ await wrapper() == "test"
+
+ func.call_count = 3
+
+
+class TestLeetcodeData:
+ _question_detail_singleton: Optional[
+ leetcode.models.graphql_question_detail.GraphqlQuestionDetail
+ ] = None
+ _leetcode_data_singleton: Optional[
+ leetcode_anki.helpers.leetcode.LeetcodeData
+ ] = None
+
+ @property
+ def _question_details(
+ self,
+ ) -> leetcode.models.graphql_question_detail.GraphqlQuestionDetail:
+ question_detail = self._question_detail_singleton
+
+ if not question_detail:
+ raise ValueError("Question detail must not be None")
+
+ return question_detail
+
+ @property
+ def _leetcode_data(self) -> leetcode_anki.helpers.leetcode.LeetcodeData:
+ leetcode_data = self._leetcode_data_singleton
+
+ if not leetcode_data:
+ raise ValueError("Leetcode data must not be None")
+
+ return leetcode_data
+
+ @mock.patch("diskcache.Cache", mock.Mock(side_effect=lambda _: {}))
+ @mock.patch("os.path.exists", mock.Mock(return_value=True))
+ @mock.patch("leetcode_anki.helpers.leetcode._get_leetcode_api_client")
+ def setup(self, leetcode_api: leetcode.api.default_api.DefaultApi) -> None:
+ self._question_detail_singleton = leetcode.models.graphql_question_detail.GraphqlQuestionDetail(
+ freq_bar=1.1,
+ question_id="1",
+ question_frontend_id="1",
+ bound_topic_id=1,
+ title="test title",
+ content="test content",
+ translated_title="test",
+ translated_content="test translated content",
+ is_paid_only=False,
+ difficulty="Hard",
+ likes=1,
+ dislikes=1,
+ is_liked=False,
+ similar_questions="{}",
+ contributors=[
+ leetcode.models.graphql_question_contributor.GraphqlQuestionContributor(
+ username="testcontributor",
+ profile_url="test://profile/url",
+ avatar_url="test://avatar/url",
+ ),
+ ],
+ lang_to_valid_playground="{}",
+ topic_tags=[
+ leetcode.models.graphql_question_topic_tag.GraphqlQuestionTopicTag(
+ name="test tag",
+ slug="test-tag",
+ translated_name="translated test tag",
+ typename="test type name",
+ )
+ ],
+ company_tag_stats="{}",
+ code_snippets="{}",
+ stats='{"totalSubmissionRaw": 1, "totalAcceptedRaw": 1}',
+ hints=["test hint 1", "test hint 2"],
+ solution=[
+ leetcode.models.graphql_question_solution.GraphqlQuestionSolution(
+ id=1,
+ can_see_detail=False,
+ typename="test type name",
+ ),
+ ],
+ status="ac",
+ sample_test_case="test case",
+ meta_data="{}",
+ judger_available=False,
+ judge_type="large",
+ mysql_schemas="test schema",
+ enable_run_code=False,
+ enable_test_mode=False,
+ env_info="{}",
+ )
+ self._leetcode_data_singleton = leetcode_anki.helpers.leetcode.LeetcodeData()
+
+ def test_init(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ @pytest.mark.asyncio
+ async def test_get_description(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+ assert (await self._leetcode_data.description("test")) == "test content"
+
+ @pytest.mark.asyncio
+ async def test_submissions(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+ assert (await self._leetcode_data.submissions_total("test")) == 1
+ assert (await self._leetcode_data.submissions_accepted("test")) == 1
+
+ @pytest.mark.asyncio
+ async def test_difficulty(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ self._leetcode_data._cache["test"].difficulty = "Easy"
+ assert "Easy" in (await self._leetcode_data.difficulty("test"))
+
+ self._leetcode_data._cache["test"].difficulty = "Medium"
+ assert "Medium" in (await self._leetcode_data.difficulty("test"))
+
+ self._leetcode_data._cache["test"].difficulty = "Hard"
+ assert "Hard" in (await self._leetcode_data.difficulty("test"))
+
+ @pytest.mark.asyncio
+ async def test_paid(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ assert (await self._leetcode_data.paid("test")) is False
+
+ @pytest.mark.asyncio
+ async def test_problem_id(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ assert (await self._leetcode_data.problem_id("test")) == "1"
+
+ @pytest.mark.asyncio
+ async def test_likes(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ assert (await self._leetcode_data.likes("test")) == 1
+ assert (await self._leetcode_data.dislikes("test")) == 1
+
+ @pytest.mark.asyncio
+ async def test_tags(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ assert (await self._leetcode_data.tags("test")) == ["test-tag"]
+
+ @pytest.mark.asyncio
+ async def test_freq_bar(self) -> None:
+ self._leetcode_data._cache["test"] = self._question_details
+
+ assert (await self._leetcode_data.freq_bar("test")) == 1.1
+
+ @mock.patch("time.sleep", mock.Mock())
+ @pytest.mark.asyncio
+ async def test_get_problem_data(self) -> None:
+ data = leetcode.models.graphql_data.GraphqlData(question=self._question_details)
+ response = leetcode.models.graphql_response.GraphqlResponse(data=data)
+ self._leetcode_data._api_instance.graphql_post.return_value = response
+
+ assert (
+ await self._leetcode_data._get_problem_data("test")
+ ) == self._question_details
+
+ assert self._leetcode_data._cache["test"] == self._question_details